2020年8月

Linux 基金会考虑放弃采用电子邮件列表的 Linux 内核的贡献方式

多年以来,Linux 内核邮件列表一直作为内核开发人员之间进行协作的主要工具而存在,纯文本电子邮件是内核开发的入门门槛。电子邮件机制确实是有效的,但它对新的贡献者来说也确实是一项障碍。Linux 基金会目前的想法是,从现有的基于文本的电子邮件系统迁移,或在此基础上拓展一个新近成长起来的开发者更为熟悉的方式。

来源:开源中国

拍一拍:当年能创新出 Git 的 Linux 内核开发者们,却一直守着上世纪的电子邮件列表不放,是 Linus 老了还是成长为大型软件组织的 Linux 内核开发者们僵化了?

跨平台桌面应用开发工具 Electron 10.0.0 发布

根据项目的支持政策, Electron 7.xy 已终止支持。鼓励开发人员和应用程序升级到新版本的 Electron。

来源:开源中国

拍一拍:比较有意思的是,原本为了跨平台应用而开发的 Java,在这方面越来越不受欢迎了。

Chrome 85 启用新技术提升 10% 性能

Google 释出了 Chrome 85。新版的一个重要变化是在编译时启用了名为 Profile Guided Optimization 的技术优化性能。Google 称,页面加载速度提升了 10%。Chrome 85 的其它特性包括标签组支持折叠和展开,改进 PDF,新的开发者特性。

来源:solidot

拍一拍:Chrome 形成压倒性优势的背后,与他们不断进取采用新技术也有很大关系。

通过管理一套图书的完整代码示例,来探索轻量级的 RESTful 服务。

Web 服务,以这样或那样的形式,已经存在了近二十年。比如,XML-RPC 服务出现在 90 年代后期,紧接着是用 SOAP 分支编写的服务。在 XML-RPC 和 SOAP 这两个开拓者之后出现后不久,REST 架构风格的服务在大约 20 年前也出现了。REST 风格(以下简称 Restful)服务现在主导了流行的网站,比如 eBay、Facebook 和 Twitter。尽管分布式计算的 Web 服务有很多替代品(如 Web 套接字、微服务和远程过程调用的新框架),但基于 Restful 的 Web 服务依然具有吸引力,原因如下:

  • Restful 服务建立在现有的基础设施和协议上,特别是 Web 服务器和 HTTP/HTTPS 协议。一个拥有基于 HTML 的网站的组织可以很容易地为客户添加 Web 服务,这些客户对数据和底层功能更感兴趣,而不是对 HTML 的表现形式感兴趣。比如,亚马逊就率先通过网站和 Web 服务(基于 SOAP 或 Restful)提供相同的信息和功能。
  • Restful 服务将 HTTP 当作 API,因此避免了复杂的软件分层,这种分层是基于 SOAP 的 Web 服务的明显特征。比如,Restful API 支持通过 HTTP 命令(POST-GET-PUT-DELETE)进行标准的 CRUD(增加-读取-更新-删除)操作;通过 HTTP 状态码可以知道请求是否成功或者为什么失败。
  • Restful Web 服务可以根据需要变得简单或复杂。Restful 是一种风格,实际上是一种非常灵活的风格,而不是一套关于如何设计和构造服务的规定。(伴随而来的缺点是,可能很难确定哪些服务不能算作 Restful 服务。)
  • 作为使用者或者客户端,Restful Web 服务与语言和平台无关。客户端发送 HTTP(S) 请求,并以适合现代数据交换的格式(如 JSON)接收文本响应。
  • 几乎每一种通用编程语言都至少对 HTTP/HTTPS 有足够的(通常是强大的)支持,这意味着 Web 服务的客户端可以用这些语言来编写。

这篇文章将通过一段完整的 Java 代码示例来探讨轻量级的 Restful 服务。

基于 Restful 的“小说” Web 服务

基于 Restful 的“小说” web 服务包含三个程序员定义的类:

  • Novel 类代表一个小说,只有三个属性:机器生成的 ID、作者和标题。属性可以根据实际情况进行扩展,但我还是想让这个例子看上去更简单一些。
  • Novels 类包含了用于各种任务的工具类:将一个 Novel 或者它们的列表的纯文本编码转换成 XML 或者 JSON;支持在小说集合上进行 CRUD 操作;以及从存储在文件中的数据初始化集合。Novels 类在 Novel 实例和 servlet 之间起中介作用。
  • NovelsServlet 类是从 HttpServlet 中继承的,HttpServlet 是一段健壮且灵活的代码,自 90 年代末的早期企业级 Java 就已经存在了。对于客户端的 CRUD 请求,servlet 可以当作 HTTP 的端点。 servlet 代码主要用于处理客户端的请求和生成相应的响应,而将复杂的细节留给 Novels 类中的工具类进行处理。

一些 Java 框架,比如 Jersey(JAX-RS)和 Restlet,就是为 Restful 服务设计的。尽管如此,HttpServlet 本身为完成这些服务提供了轻量、灵活、强大且充分测试过的 API。我会通过下面的“小说”例子来说明。

部署“小说” Web 服务

当然,部署“小说” Web 服务需要一个 Web 服务器。我的选择是 Tomcat,但是如果该服务托管在 Jetty 或者甚至是 Java 应用服务器上,那么这个服务应该至少可以工作(著名的最后一句话!)。在我的网站上有总结了如何安装 Tomcat 的 README 文件和代码。还有一个附带文档的 Apache Ant 脚本,可以用来构建“小说”服务(或者任何其他服务或网站),并且将它部署在 Tomcat 或相同的服务。

Tomcat 可以从它的官网上下载。当你在本地安装后,将 TOMCAT_HOME 设置为安装目录。有两个子目录值得关注:

  • TOMCAT_HOME/bin 目录包含了类 Unix 系统(startup.shshutdown.sh)和 Windows(startup.batshutdown.bat) 的启动和停止脚本。Tomcat 作为 Java 应用程序运行。Web 服务器的 servlet 容器叫做 Catalina。(在 Jetty 中,Web 服务器和容器的名字一样。)当 Tomcat 启动后,在浏览器中输入 http://localhost:8080/可以查看详细文档,包括示例。
  • TOMCAT_HOME/webapps 目录是已部署的 Web 网站和服务的默认目录。部署网站或 Web 服务的直接方法是复制以 .war 结尾的 JAR 文件(也就是 WAR 文件)到 TOMCAT_HOME/webapps 或它的子目录下。然后 Tomcat 会将 WAR 文件解压到它自己的目录下。比如,Tomcat 会将 novels.war 文件解压到一个叫做 novels 的子目录下,并且保留 novels.war 文件。一个网站或 Web 服务可以通过删除 WAR 文件进行移除,也可以用一个新版 WAR 文件来覆盖已有文件进行更新。顺便说一下,调试网站或服务的第一步就是检查 Tomcat 已经正确解压 WAR 文件;如果没有的话,网站或服务就无法发布,因为代码或配置中有致命错误。
  • 因为 Tomcat 默认会监听 8080 端口上的 HTTP 请求,所以本机上的 URL 请求以 http://localhost:8080/ 开始。

通过添加不带 .war 后缀的 WAR 文件名来访问由程序员部署的 WAR 文件:

http://locahost:8080/novels/

如果服务部署在 TOMCAT_HOME 下的一个子目录中(比如,myapps),这会在 URL 中反映出来:

http://locahost:8080/myapps/novels/

我会在靠近文章结尾处的测试部分提供这部分的更多细节。

如前所述,我的主页上有一个包含 Ant 脚本的 ZIP 文件,这个文件可以编译并且部署网站或者服务。(这个 ZIP 文件中也包含一个 novels.war 的副本。)对于“小说”这个例子,命令的示例(% 是命令行提示符)如下:

% ant -Dwar.name=novels deploy

这个命令首先会编译 Java 源代码,并且创建一个可部署的 novels.war 文件,然后将这个文件保存在当前目录中,再复制到 TOMCAT_HOME/webapps 目录中。如果一切顺利,GET 请求(使用浏览器或者命令行工具,比如 curl)可以用来做一个测试:

% curl http://localhost:8080/novels/

默认情况下,Tomcat 设置为 热部署 hot deploys :Web 服务器不需要关闭就可以进行部署、更新或者移除一个 web 应用。

“小说”服务的代码

让我们回到“小说”这个例子,不过是在代码层面。考虑下面的 Novel 类:

例 1:Novel 类

package novels;

import java.io.Serializable;

public class Novel implements Serializable, Comparable<Novel> {
    static final long serialVersionUID = 1L;
    private String author;
    private String title;
    private int id;

    public Novel() { }

    public void setAuthor(final String author) { this.author = author; }
    public String getAuthor() { return this.author; }
    public void setTitle(final String title) { this.title = title; }
    public String getTitle() { return this.title; }
    public void setId(final int id) { this.id = id; }
    public int getId() { return this.id; }

    public int compareTo(final Novel other) { return this.id - other.id; }
}

这个类实现了 Comparable 接口中的 compareTo 方法,因为 Novel 实例是存储在一个线程安全的无序 ConcurrentHashMap 中。在响应查看集合的请求时,“小说”服务会对从映射中提取的集合(一个 ArrayList)进行排序;compareTo 的实现通过 Novel 的 ID 将它按升序排序。

Novels 类中包含多个实用工具函数:

例 2:Novels 实用工具类

package novels;

import java.io.IOException;
import java.io.File;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.BufferedReader;
import java.nio.file.Files;
import java.util.stream.Stream;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.Collections;
import java.beans.XMLEncoder;
import javax.servlet.ServletContext; // not in JavaSE
import org.json.JSONObject;
import org.json.XML;

public class Novels {
    private final String fileName = "/WEB-INF/data/novels.db";
    private ConcurrentMap<Integer, Novel> novels;
    private ServletContext sctx;
    private AtomicInteger mapKey;

    public Novels() {
        novels = new ConcurrentHashMap<Integer, Novel>();
        mapKey = new AtomicInteger();
    }

    public void setServletContext(ServletContext sctx) { this.sctx = sctx; }
    public ServletContext getServletContext() { return this.sctx; }

    public ConcurrentMap<Integer, Novel> getConcurrentMap() {
        if (getServletContext() == null) return null; // not initialized
        if (novels.size() < 1) populate();
        return this.novels;
    }

    public String toXml(Object obj) { // default encoding
        String xml = null;
        try {
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            XMLEncoder encoder = new XMLEncoder(out);
            encoder.writeObject(obj);
            encoder.close();
            xml = out.toString();
        }
        catch(Exception e) { }
        return xml;
    }

    public String toJson(String xml) { // option for requester
        try {
            JSONObject jobt = XML.toJSONObject(xml);
            return jobt.toString(3); // 3 is indentation level
        }
        catch(Exception e) { }
        return null;
    }

    public int addNovel(Novel novel) {
        int id = mapKey.incrementAndGet();
        novel.setId(id);
        novels.put(id, novel);
        return id;
    }

    private void populate() {
        InputStream in = sctx.getResourceAsStream(this.fileName);
        // Convert novel.db string data into novels.
        if (in != null) {
            try {
                InputStreamReader isr = new InputStreamReader(in);
                BufferedReader reader = new BufferedReader(isr);

                String record = null;
                while ((record = reader.readLine()) != null) {
                    String[] parts = record.split("!");
                    if (parts.length == 2) {
                        Novel novel = new Novel();
                        novel.setAuthor(parts[0]);
                        novel.setTitle(parts[1]);
                        addNovel(novel); // sets the Id, adds to map
                    }
                }
                in.close();
            }
            catch (IOException e) { }
        }
    }
}

最复杂的方法是 populate,这个方法从一个包含在 WAR 文件中的文本文件读取。这个文本文件包括了“小说”的初始集合。要打开此文件,populate 方法需要 ServletContext,这是一个 Java 映射类型,包含了关于嵌入在 servlet 容器中的 servlet 的所有关键信息。这个文本文件有包含了像下面这样的记录:

Jane Austen!Persuasion

这一行被解析为两部分(作者和标题),由感叹号(!)分隔。然后这个方法创建一个 Novel 实例,设置作者和标题属性,并且将“小说”加到容器中,保存在内存中。

Novels 类也有一些实用工具函数,可以将“小说”容器编码为 XML 或 JSON,取决于发出请求的人所要求的格式。默认是 XML 格式,但是也可以请求 JSON 格式。一个轻量级的 XML 转 JSON 包提供了 JSON。下面是关于编码的更多细节。

例 3:NovelsServlet 类

package novels;

import java.util.concurrent.ConcurrentMap;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Arrays;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.OutputStream;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.beans.XMLEncoder;
import org.json.JSONObject;
import org.json.XML;

public class NovelsServlet extends HttpServlet {
    static final long serialVersionUID = 1L;
    private Novels novels; // back-end bean

    // Executed when servlet is first loaded into container.
    @Override
    public void init() {
        this.novels = new Novels();
        novels.setServletContext(this.getServletContext());
    }

    // GET /novels
    // GET /novels?id=1
    @Override
    public void doGet(HttpServletRequest request, HttpServletResponse response) {
        String param = request.getParameter("id");
        Integer key = (param == null) ? null : Integer.valueOf((param.trim()));

        // Check user preference for XML or JSON by inspecting
        // the HTTP headers for the Accept key.
        boolean json = false;
        String accept = request.getHeader("accept");
        if (accept != null && accept.contains("json")) json = true;

        // If no query string, assume client wants the full list.
        if (key == null) {
            ConcurrentMap<Integer, Novel> map = novels.getConcurrentMap();
            Object list = map.values().toArray();
            Arrays.sort(list);

            String payload = novels.toXml(list);        // defaults to Xml
            if (json) payload = novels.toJson(payload); // Json preferred?
            sendResponse(response, payload);
        }
        // Otherwise, return the specified Novel.
        else {
            Novel novel = novels.getConcurrentMap().get(key);
            if (novel == null) { // no such Novel
                String msg = key + " does not map to a novel.\n";
                sendResponse(response, novels.toXml(msg));
            }
            else { // requested Novel found
                if (json) sendResponse(response, novels.toJson(novels.toXml(novel)));
                else sendResponse(response, novels.toXml(novel));
            }
        }
    }

    // POST /novels
    @Override
    public void doPost(HttpServletRequest request, HttpServletResponse response) {
        String author = request.getParameter("author");
        String title = request.getParameter("title");

        // Are the data to create a new novel present?
        if (author == null || title == null)
            throw new RuntimeException(Integer.toString(HttpServletResponse.SC_BAD_REQUEST));

        // Create a novel.
        Novel n = new Novel();
        n.setAuthor(author);
        n.setTitle(title);

        // Save the ID of the newly created Novel.
        int id = novels.addNovel(n);

        // Generate the confirmation message.
        String msg = "Novel " + id + " created.\n";
        sendResponse(response, novels.toXml(msg));
    }

    // PUT /novels
    @Override
    public void doPut(HttpServletRequest request, HttpServletResponse response) {
        /\* A workaround is necessary for a PUT request because Tomcat does not
 generate a workable parameter map for the PUT verb. \*/
        String key = null;
        String rest = null;
        boolean author = false;

        /\* Let the hack begin. \*/
        try {
            BufferedReader br =
                new BufferedReader(new InputStreamReader(request.getInputStream()));
            String data = br.readLine();
            /\* To simplify the hack, assume that the PUT request has exactly
 two parameters: the id and either author or title. Assume, further,
 that the id comes first. From the client side, a hash character
 # separates the id and the author/title, e.g.,

 id=33#title=War and Peace
 \*/
            String[] args = data.split("#");      // id in args[0], rest in args[1]
            String[] parts1 = args[0].split("="); // id = parts1[1]
            key = parts1[1];

            String[] parts2 = args[1].split("="); // parts2[0] is key
            if (parts2[0].contains("author")) author = true;
            rest = parts2[1];
        }
        catch(Exception e) {
            throw new RuntimeException(Integer.toString(HttpServletResponse.SC_INTERNAL_SERVER_ERROR));
        }

        // If no key, then the request is ill formed.
        if (key == null)
            throw new RuntimeException(Integer.toString(HttpServletResponse.SC_BAD_REQUEST));

        // Look up the specified novel.
        Novel p = novels.getConcurrentMap().get(Integer.valueOf((key.trim())));
        if (p == null) { // not found
            String msg = key + " does not map to a novel.\n";
            sendResponse(response, novels.toXml(msg));
        }
        else { // found
            if (rest == null) {
                throw new RuntimeException(Integer.toString(HttpServletResponse.SC_BAD_REQUEST));
            }
            // Do the editing.
            else {
                if (author) p.setAuthor(rest);
                else p.setTitle(rest);

                String msg = "Novel " + key + " has been edited.\n";
                sendResponse(response, novels.toXml(msg));
            }
        }
    }

    // DELETE /novels?id=1
    @Override
    public void doDelete(HttpServletRequest request, HttpServletResponse response) {
        String param = request.getParameter("id");
        Integer key = (param == null) ? null : Integer.valueOf((param.trim()));
        // Only one Novel can be deleted at a time.
        if (key == null)
            throw new RuntimeException(Integer.toString(HttpServletResponse.SC_BAD_REQUEST));
        try {
            novels.getConcurrentMap().remove(key);
            String msg = "Novel " + key + " removed.\n";
            sendResponse(response, novels.toXml(msg));
        }
        catch(Exception e) {
            throw new RuntimeException(Integer.toString(HttpServletResponse.SC_INTERNAL_SERVER_ERROR));
        }
    }

    // Methods Not Allowed
    @Override
    public void doTrace(HttpServletRequest request, HttpServletResponse response) {
        throw new RuntimeException(Integer.toString(HttpServletResponse.SC_METHOD_NOT_ALLOWED));
    }

    @Override
    public void doHead(HttpServletRequest request, HttpServletResponse response) {
        throw new RuntimeException(Integer.toString(HttpServletResponse.SC_METHOD_NOT_ALLOWED));
    }

    @Override
    public void doOptions(HttpServletRequest request, HttpServletResponse response) {
        throw new RuntimeException(Integer.toString(HttpServletResponse.SC_METHOD_NOT_ALLOWED));
    }

    // Send the response payload (Xml or Json) to the client.
    private void sendResponse(HttpServletResponse response, String payload) {
        try {
            OutputStream out = response.getOutputStream();
            out.write(payload.getBytes());
            out.flush();
        }
        catch(Exception e) {
            throw new RuntimeException(Integer.toString(HttpServletResponse.SC_INTERNAL_SERVER_ERROR));
        }
    }
}

上面的 NovelsServlet 类继承了 HttpServlet 类,HttpServlet 类继承了 GenericServlet 类,后者实现了 Servlet 接口:

NovelsServlet extends HttpServlet extends GenericServlet implements Servlet

从名字可以很清楚的看出来,HttpServlet 是为实现 HTTP(S) 上的 servlet 设计的。这个类提供了以标准 HTTP 请求动词(官方说法, 方法 methods )命名的空方法:

  • doPost (Post = 创建)
  • doGet (Get = 读取)
  • doPut (Put = 更新)
  • doDelete (Delete = 删除)

其他一些 HTTP 动词也会涉及到。HttpServlet 的子类,比如 NovelsServlet,会重载相关的 do 方法,并且保留其他方法为 no-ops NovelsServlet 重载了七个 do 方法。

每个 HttpServlet 的 CRUD 方法都有两个相同的参数。下面以 doPost 为例:

public void doPost(HttpServletRequest request, HttpServletResponse response) {

request 参数是一个 HTTP 请求信息的映射,而 response 提供了一个返回给请求者的输出流。像 doPost 的方法,结构如下:

  • 读取 request 信息,采取任何适当的措施生成一个响应。如果该信息丢失或者损坏了,就会生成一个错误。
  • 使用提取的请求信息来执行适当的 CRUD 操作(在本例中,创建一个 Novel),然后使用 response 输出流为请求者编码一个适当的响应。在 doPost 例子中,响应就是已经成功生成一个新“小说”并且添加到容器中的一个确认。当响应被发送后,输出流就关闭了,同时也将连接关闭了。

关于方法重载的更多内容

HTTP 请求的格式相对比较简单。下面是一个非常熟悉的 HTTP 1.1 的格式,注释由双井号分隔:

GET /novels              ## start line
Host: localhost:8080     ## header element
Accept-type: text/plain  ## ditto
...
[body]                   ## POST and PUT only

第一行由 HTTP 动词(在本例中是 GET)和以名词(在本例中是 novels)命名目标资源的 URI 开始。报头中包含键-值对,用冒号分隔左面的键和右面的值。报头中的键 Host(大小写敏感)是必须的;主机名 localhost 是当前机器上的本地符号地址,8080 端口是 Tomcat web 服务器上等待 HTTP 请求的默认端口。(默认情况下,Tomcat 在 8443 端口上监听 HTTP 请求。)报头元素可以以任意顺序出现。在这个例子中,Accept-type 报头的值是 MIME 类型 text/plain

一些请求(特别是 POSTPUT)会有报文,而其他请求(特别是 GETDELETE)没有。如果有报文(可能为空),以两个换行符将报头和报文分隔开;HTTP 报文包含一系列键-值对。对于无报文的请求,比如说查询字符串,报头元素就可以用来发送信息。下面是一个用 ID 2 对 /novels 资源的 GET 请求:

GET /novels?id=2

通常来说,查询字符串以问号开始,并且包含一个键-值对,尽管这个键-值可能值为空。

带有 getParametergetParameterMap 等方法的 HttpServlet 很好地回避了有报文和没有报文的 HTTP 请求之前的差异。在“小说”例子中,getParameter 方法用来从 GETPOSTDELETE 方法中提取所需的信息。(处理 PUT请求需要更底层的代码,因为 Tomcat 没有提供可以解析 PUT 请求的参数映射。)下面展示了一段在 NovelsServlet中被重载的 doPost 方法:

@Override
public void doPost(HttpServletRequest request, HttpServletResponse response) {
   String author = request.getParameter("author");
   String title = request.getParameter("title");
   ...

对于没有报文的 DELETE 请求,过程基本是一样的:

@Override
public void doDelete(HttpServletRequest request, HttpServletResponse response) {
   String param = request.getParameter("id"); // id of novel to be removed
   ...

doGet 方法需要区分 GET 请求的两种方式:一种是“获得所有”,而另一种是“获得某一个”。如果 GET 请求 URL 中包含一个键是一个 ID 的查询字符串,那么这个请求就被解析为“获得某一个”:

http://localhost:8080/novels?id=2  ## GET specified

如果没有查询字符串,GET 请求就会被解析为“获得所有”:

http://localhost:8080/novels       ## GET all

一些值得注意的细节

“小说”服务的设计反映了像 Tomcat 这样基于 Java 的 web 服务器是如何工作的。在启动时,Tomcat 构建一个线程池,从中提取请求处理程序,这种方法称为 “ 每个请求一个线程 one thread per request ” 模型。现在版本的 Tomcat 使用非阻塞 I/O 来提高个性能。

“小说”服务是作为 NovelsServlet 类的单个实例来执行的,该实例也就维护了一个“小说”集合。相应的,也就会出现竞态条件,比如出现两个请求同时被处理:

  • 一个请求向集合中添加一个新“小说”。
  • 另一个请求获得集合中的所有“小说”。

这样的结果是不确定的,取决与 的操作是以怎样的顺序进行操作的。为了避免这个问题,“小说”服务使用了线程安全的 ConcurrentMap。这个映射的关键是生成了一个线程安全的 AtomicInteger。下面是相关的代码片段:

public class Novels {
    private ConcurrentMap<Integer, Novel> novels;
    private AtomicInteger mapKey;
    ...

默认情况下,对客户端请求的响应被编码为 XML。为了简单,“小说”程序使用了以前的 XMLEncoder 类;另一个包含更丰富功能的方式是使用 JAX-B 库。代码很简单:

public String toXml(Object obj) { // default encoding
   String xml = null;
   try {
      ByteArrayOutputStream out = new ByteArrayOutputStream();
      XMLEncoder encoder = new XMLEncoder(out);
      encoder.writeObject(obj);
      encoder.close();
      xml = out.toString();
   }
   catch(Exception e) { }
   return xml;
}

Object 参数要么是一个有序的“小说” ArraList(用以响应“ 获得所有 get all ”请求),要么是一个 Novel 实例(用以响应“ 获得一个 get one ”请求),又或者是一个 String(确认消息)。

如果 HTTP 请求报头指定 JSON 作为所需要的类型,那么 XML 就被转化成 JSON。下面是 NovelsServlet 中的 doGet 方法中的检查:

String accept = request.getHeader("accept"); // "accept" is case insensitive
if (accept != null && accept.contains("json")) json = true;

Novels类中包含了 toJson 方法,可以将 XML 转换成 JSON:

public String toJson(String xml) { // option for requester
   try {
      JSONObject jobt = XML.toJSONObject(xml);
      return jobt.toString(3); // 3 is indentation level
   }
   catch(Exception e) { }
   return null;
}

NovelsServlet会对各种类型进行错误检查。比如,POST 请求应该包含新“小说”的作者和标题。如果有一个丢了,doPost 方法会抛出一个异常:

if (author == null || title == null)
   throw new RuntimeException(Integer.toString(HttpServletResponse.SC_BAD_REQUEST));

SC_BAD_REQUEST 中的 SC 代表的是 状态码 status code BAD_REQUEST 的标准 HTTP 数值是 400。如果请求中的 HTTP 动词是 TRACE,会返回一个不同的状态码:

public void doTrace(HttpServletRequest request, HttpServletResponse response) {
   throw new RuntimeException(Integer.toString(HttpServletResponse.SC_METHOD_NOT_ALLOWED));
}

测试“小说”服务

用浏览器测试 web 服务会很不顺手。在 CRUD 动词中,现代浏览器只能生成 POST(创建)和 GET(读取)请求。甚至从浏览器发送一个 POST 请求都有点不好办,因为报文需要包含键-值对;这样的测试通常通过 HTML 表单完成。命令行工具,比如说 curl,是一个更好的选择,这个部分展示的一些 curl 命令,已经包含在我网站的 ZIP 文件中了。

下面是一些测试样例,没有展示相应的输出结果:

% curl localhost:8080/novels/
% curl localhost:8080/novels?id=1
% curl --header "Accept: application/json" localhost:8080/novels/

第一条命令请求所有“小说”,默认是 XML 编码。第二条命令请求 ID 为 1 的“小说”,XML 编码。最后一条命令通过 application/json 添加了 Accept 报头元素,作为所需要的 MIME 类型。“ 获得一个 get one ”命令也可以用这个报头。这些请求用了 JSON 而不是 XML 编码作为响应。

下面两条命令在集合中创建了一个新“小说”,并且确认添加了进去:

% curl --request POST --data "author=Tolstoy&amp;title=War and Peace" localhost:8080/novels/
% curl localhost:8080/novels?id=4

curl 中的 PUT 命令与 POST 命令相似,不同的地方是 PUT 的报文没有使用标准的语法。在 NovelsServlet 中关于 doPut 方法的文档中有详细的介绍,但是简单来说,Tomcat 不会对 PUT 请求生成合适的映射。下面是一个 PUT 命令和确认命令的的例子:

% curl --request PUT --data "id=3#title=This is an UPDATE" localhost:8080/novels/
% curl localhost:8080/novels?id=3

第二个命令确认了集合已经更新。

最后,DELETE 命令会正常运行:

% curl --request DELETE localhost:8080/novels?id=2
% curl localhost:8080/novels/

这个请求是删除 ID 为 2 的“小说”。第二个命令会显示剩余的“小说”。

web.xml 配置文件

尽管官方规定它是可选的,web.xml 配置文件是一个生产级别网站或服务的重要组成部分。这个配置文件可以配置独立于代码的路由、安全性,或者网站或服务的其他功能。“小说”服务的配置通过为该服务的请求分配一个 URL 模式来配置路由:

<xml version = "1.0" encoding = "UTF-8">
<web-app>
   <servlet>
     <servlet-name>novels</servlet-name>
     <servlet-class>novels.NovelsServlet</servlet-class>
   </servlet>
   <servlet-mapping>
     <servlet-name>novels</servlet-name>
     <url-pattern>/*</url-pattern>
   </servlet-mapping>
</web-app>

servlet-name 元素为 servlet 全名(novels.NovelsServlet)提供了一个缩写(novels),然后这个名字在下面的 servlet-mapping 元素中使用。

回想一下,一个已部署服务的 URL 会在端口号后面有 WAR 文件的文件名:

http://localhost:8080/novels/

端口号后斜杠后的 URI,是所请求资源的“路径”,在这个例子中,就是“小说”服务。因此,novels 出现在了第一个单斜杠后。

web.xml 文件中,url-patter 被指定为 /*,代表 “以 /novels 为起始的任意路径”。假设 Tomcat 遇到了一个不存在的 URL,像这样:

http://localhost:8080/novels/foobar/

web.xml 配置也会指定这个请求被分配到“小说” servlet 中,因为 /* 模式也包含 /foobar。因此,这个不存在的 URL 也会得到像上面合法路径的相同结果。

生产级别的配置文件可能会包含安全相关的信息,包括 连接级别 wire-level 用户角色 users-roles 。即使在这种情况下,配置文件的大小也只会是这个例子中的两到三倍大。

总结

HttpServlet 是 Java web 技术的核心。像“小说”这样的网站或 web 服务继承了这个类,并且根据需求重载了相应的 do 动词方法。像 Jersay(JAX-RS)或 Restlet 这样的 Restful 框架通过提供定制的 servlet 完成了基本相同的功能,针对框架中的 web 应用程序的请求,这个 servlet 扮演着 HTTP(S) 终端 endpoint 的角色。

当然,基于 servlet 的应用程序可以访问 web 应用程序中所需要的任何 Java 库。如果应用程序遵循 关注点分离 separation-of-concerns 原则,那么 servlet 代码仍然相当简单:代码会检查请求,如果存在缺陷,就会发出适当的错误;否则,代码会调用所需要的功能(比如,查询数据库,以特定格式为响应编码),然后向请求这发送响应。HttpServletRequestHttpServletReponse 类型使得读取请求和编写响应变得简单。

Java 的 API 可以从非常简单变得相当复杂。如果你需要用 Java 交付一些 Restful 服务的话,我的建议是在做其他事情之前先尝试一下简单的 HttpServlet


via: https://opensource.com/article/20/7/restful-services-java

作者:Marty Kalin 选题:lujun9972 译者:Yufei-Yan 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

了解如何处理转储文件将帮你找到应用中难以重现的 bug。

崩溃转储、内存转储、核心转储、系统转储……这些全都会产生同样的产物:一个包含了当应用崩溃时,在那个特定时刻应用的内存状态的文件。

这是一篇指导文章,你可以通过克隆示例的应用仓库来跟随学习:

git clone https://github.com/hANSIc99/core_dump_example.git

信号如何关联到转储

信号是操作系统和用户应用之间的进程间通讯。Linux 使用 POSIX 标准中定义的信号。在你的系统上,你可以在 /usr/include/bits/signum-generic.h 找到标准信号的定义。如果你想知道更多关于在你的应用程序中使用信号的信息,这有一个信息丰富的 signal 手册页。简单地说,Linux 基于预期的或意外的信号来触发进一步的活动。

当你退出一个正在运行的应用程序时,应用程序通常会收到 SIGTERM 信号。因为这种类型的退出信号是预期的,所以这个操作不会创建一个内存转储。

以下信号将导致创建一个转储文件(来源:GNU C库):

  • SIGFPE:错误的算术操作
  • SIGILL:非法指令
  • SIGSEGV:对存储的无效访问
  • SIGBUS:总线错误
  • SIGABRT:程序检测到的错误,并通过调用 abort() 来报告
  • SIGIOT:这个信号在 Fedora 上已经过时,过去在 PDP-11 上用 abort() 时触发,现在映射到 SIGABRT

创建转储文件

导航到 core_dump_example 目录,运行 make,并使用 -c1 开关执行该示例二进制:

./coredump -c1

该应用将以状态 4 退出,带有如下错误:

 title=

“Abgebrochen (Speicherabzug geschrieben) ”(LCTT 译注:这是德语,应该是因为本文作者系统是德语环境)大致翻译为“分段故障(核心转储)”。

是否创建核心转储是由运行该进程的用户的资源限制决定的。你可以用 ulimit 命令修改资源限制。

检查当前创建核心转储的设置:

ulimit -c

如果它输出 unlimited,那么它使用的是(建议的)默认值。否则,用以下方法纠正限制:

ulimit -c unlimited

要禁用创建核心转储,可以设置其大小为 0:

ulimit -c 0

这个数字指定了核心转储文件的大小,单位是块。

什么是核心转储?

内核处理核心转储的方式定义在:

/proc/sys/kernel/core_pattern

我运行的是 Fedora 31,在我的系统上,该文件包含的内容是:

/usr/lib/systemd/systemd-coredump %P %u %g %s %t %c %h

这表明核心转储被转发到 systemd-coredump 工具。在不同的 Linux 发行版中,core_pattern 的内容会有很大的不同。当使用 systemd-coredump 时,转储文件被压缩保存在 /var/lib/systemd/coredump 下。你不需要直接接触这些文件,你可以使用 coredumpctl。比如说:

coredumpctl list

会显示系统中保存的所有可用的转储文件。

使用 coredumpctl dump,你可以从最后保存的转储文件中检索信息:

[stephan@localhost core_dump_example]$ ./coredump 
Application started…

(…….)

Message: Process 4598 (coredump) of user 1000 dumped core.

Stack trace of thread 4598:
#0 0x00007f4bbaf22625 __GI_raise (libc.so.6)
#1 0x00007f4bbaf0b8d9 __GI_abort (libc.so.6)
#2 0x00007f4bbaf664af __libc_message (libc.so.6)
#3 0x00007f4bbaf6da9c malloc_printerr (libc.so.6)
#4 0x00007f4bbaf6f49c _int_free (libc.so.6)
#5 0x000000000040120e n/a (/home/stephan/Dokumente/core_dump_example/coredump)
#6 0x00000000004013b1 n/a (/home/stephan/Dokumente/core_dump_example/coredump)
#7 0x00007f4bbaf0d1a3 __libc_start_main (libc.so.6)
#8 0x000000000040113e n/a (/home/stephan/Dokumente/core_dump_example/coredump)
Refusing to dump core to tty (use shell redirection or specify — output).

这表明该进程被 SIGABRT 停止。这个视图中的堆栈跟踪不是很详细,因为它不包括函数名。然而,使用 coredumpctl debug,你可以简单地用调试器(默认为 GDB)打开转储文件。输入 bt 回溯 backtrace 的缩写)可以得到更详细的视图:

Core was generated by `./coredump -c1'.
Program terminated with signal SIGABRT, Aborted.
#0  __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:50
50  return ret;
(gdb) bt
#0  __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:50
#1  0x00007fc37a9aa8d9 in __GI_abort () at abort.c:79
#2  0x00007fc37aa054af in __libc_message (action=action@entry=do_abort, fmt=fmt@entry=0x7fc37ab14f4b "%s\n") at ../sysdeps/posix/libc_fatal.c:181
#3  0x00007fc37aa0ca9c in malloc_printerr (str=str@entry=0x7fc37ab130e0 "free(): invalid pointer") at malloc.c:5339
#4  0x00007fc37aa0e49c in _int_free (av=&lt;optimized out&gt;, p=&lt;optimized out&gt;, have_lock=0) at malloc.c:4173
#5  0x000000000040120e in freeSomething(void*) ()
#6  0x0000000000401401 in main ()

与后续帧相比,main()freeSomething() 的内存地址相当低。由于共享对象被映射到虚拟地址空间末尾的区域,可以认为 SIGABRT 是由共享库中的调用引起的。共享对象的内存地址在多次调用之间并不是恒定不变的,所以当你看到多次调用之间的地址不同时,完全可以认为是共享对象。

堆栈跟踪显示,后续的调用源于 malloc.c,这说明内存的(取消)分配可能出了问题。

在源代码中,(即使没有任何 C++ 知识)你也可以看到,它试图释放一个指针,而这个指针并没有被内存管理函数返回。这导致了未定义的行为,并导致了 SIGABRT

void freeSomething(void *ptr){
    free(ptr);
}
int nTmp = 5;
int *ptrNull = &nTmp;
freeSomething(ptrNull);

systemd 的这个 coredump 工具可以在 /etc/systemd/coredump.conf 中配置。可以在 /etc/systemd/systemd-tmpfiles-clean.timer 中配置轮换清理转储文件。

你可以在其手册页中找到更多关于 coredumpctl 的信息。

用调试符号编译

打开 Makefile 并注释掉第 9 行的最后一部分。现在应该是这样的:

CFLAGS =-Wall -Werror -std=c++11 -g

-g 开关使编译器能够创建调试信息。启动应用程序,这次使用 -c2 开关。

./coredump -c2

你会得到一个浮点异常。在 GDB 中打开该转储文件:

coredumpctl debug

这一次,你会直接被指向源代码中导致错误的那一行:

Reading symbols from /home/stephan/Dokumente/core_dump_example/coredump…
[New LWP 6218]
Core was generated by `./coredump -c2'.
Program terminated with signal SIGFPE, Arithmetic exception.
#0 0x0000000000401233 in zeroDivide () at main.cpp:29
29 nRes = 5 / nDivider;
(gdb)

键入 list 以获得更好的源代码概览:

(gdb) list
24      int zeroDivide(){
25          int nDivider = 5;
26          int nRes = 0;
27          while(nDivider &gt; 0){
28              nDivider--;
29              nRes = 5 / nDivider;
30          }
31          return nRes;
32      }

使用命令 info locals 从应用程序失败的时间点检索局部变量的值:

(gdb) info locals
nDivider = 0
nRes = 5

结合源码,可以看出,你遇到的是零除错误:

nRes = 5 / 0

结论

了解如何处理转储文件将帮助你找到并修复应用程序中难以重现的随机错误。而如果不是你的应用程序,将核心转储转发给开发人员将帮助她或他找到并修复问题。


via: https://opensource.com/article/20/8/linux-dump

作者:Stephan Avenwedde 选题:lujun9972 译者:wxy 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

微软逐渐将经典版 Edge 的诸多优秀特性迁移到新版 Edge 浏览器中

Media Foundation 是随 Windows Vista 引入的底层多媒体技术,微软已经确定该技术即将引入到新版 Edge 中。零拷贝视频捕捉是基于它的功能,在 Windows 10 系统上可以利用 GPU 内存缓冲区减少过多的电力消耗。

来源:cnBeta.COM

拍一拍:所以,Edge 当时到底失败在哪里?

基于浏览器的加密劫持在 2020 年第二季度活动量突然激增

基于浏览器的加密货币挖矿,也就是所谓的加密劫持,在今年早些时候,也就是 6 月份,出人意料地卷土重来。赛门铁克表示,与前几个季度相比,加密劫持的检测次数增加了 163%。加密劫持检测量的突然激增很可能是由路由器僵尸网络引起的。但预计不会全面卷土重来。大多数过去尝试过加密劫持行动的网络犯罪集团通常在几周后就放弃了。

来源:zdnet

拍一拍:这主要是因为最近加密货币又重新热起来了。

游戏商 Epic 起诉苹果垄断初审,法院裁决“各打五十”

法庭裁定苹果公司不得关闭游戏开发商 Epic Games 公司的开发者账户且立即生效,同时裁定苹果无需让《堡垒之夜》重回应用商店。裁决中写道:“Epic Games 和苹果公司完全可以相互起诉对方,但他们的纠纷不应该对旁观者造成影响。”

来源:网易科技

拍一拍:“你们继续打,别影响 Unreal 引擎就行。”

准备好在提到数据中心设备时,听到更多的“循环”一词。

微软正在将人工智能引入到对数百万台服务器进行分类的任务中,以确定哪些部件可以回收,在哪里回收。

新计划要求在微软全球各地的数据中心建立所谓的“ 循环中心 Circular Center ”,在那里,人工智能算法将用于从退役的服务器或其他硬件中分拣零件,并找出哪些零件可以在园区内重新使用。

微软表示,它的数据中心有超过 300 万台服务器和相关硬件,一台服务器的平均寿命约为 5 年。另外,微软正在全球范围内扩张,所以其服务器数量应该会增加。

循环中心就是要快速整理库存,而不是让过度劳累的员工疲于奔命。微软计划到 2025 年将服务器部件的重复使用率提高 90%。微软总裁 Brad Smith 在宣布这一举措的一篇博客中写道:“利用机器学习,我们将对退役的服务器和硬件进行现场处理。我们会将那些可以被我们以及客户重复使用和再利用的部件进行分类,或者出售。”

Smith 指出,如今,关于废物的数量、质量和类型,以及废物的产生地和去向,都没有一致的数据。例如,关于建造和拆除废物的数据并不一致,我们要一个标准化的方法,有更好的透明度和更高的质量。

他写道:“如果没有更准确的数据,几乎不可能了解运营决策的影响,设定什么目标,如何评估进展,以及废物去向方法的行业标准。”

根据微软的说法,阿姆斯特丹数据中心的一个循环中心试点减少了停机时间,并增加了服务器和网络部件的可用性,供其自身再利用和供应商回购。它还降低了将服务器和硬件运输到处理设施的成本,从而降低了碳排放。

循环经济 circular economy ”一词正在科技界流行。它是基于服务器硬件的循环利用,将那些已经使用了几年但仍可用的设备重新投入到其他地方服务。ITRenew 是我在几个月前介绍过的一家二手超大规模服务器的转售商,它对这个词很感兴趣。

该公司表示,首批微软循环中心将建在新的主要数据中心园区或地区。它计划最终将这些中心添加到已经存在的园区中。

微软曾明确表示要在 2030 年之前实现“碳负排放”,而这只是其中几个项目之一。近日,微软宣布在其位于盐湖城的系统开发者实验室进行了一项测试,用一套 250kW 的氢燃料电池系统为一排服务器机架连续供电 48 小时,微软表示这是以前从未做过的事情。

微软首席基础设施工程师 Mark Monroe 在一篇博客中写道:“这是我们所知道的最大的以氢气运行的计算机备用电源系统,而且它的连续测试时间最长。”他说,近年来氢燃料电池的价格大幅下降,现在已经成为柴油发电机的可行替代品,但燃烧更清洁。


via: https://www.networkworld.com/article/3570451/microsoft-uses-ai-to-boost-its-reuse-recycling-of-server-parts.html

作者:Andy Patrizio 选题:lujun9972 译者:geekpi 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

代码英雄讲述了开发人员、程序员、黑客、极客和开源反叛者如何彻底改变技术前景的真实史诗。

什么是《代码英雄》

代码英雄 Command Line Heroes 是世界领先的企业开源软件解决方案供应商红帽(Red Hat)精心制作的原创音频播客,讲述开发人员、程序员、黑客、极客和开源反叛者如何彻底改变技术前景的真实史诗。该音频博客邀请到了谷歌、NASA 等重量级企业的众多技术大牛共同讲述开源、操作系统、容器、DevOps、混合云等发展过程中的动人故事。

本文是《代码英雄》系列播客第一季(6):揭秘云计算音频脚本。

“没有什么云。这只是别人的电脑。”确切地说,还是服务器。大型云提供商提供了一种相对简单的方式来扩展工作负载。但真正的成本是什么?

在本期节目中,我们将讨论云中的战斗,说谁是赢家还很早。Major Hayden、微软的 Bridget Kromhout 等人会帮我们了解这场正在酝酿的风暴,以及它给开源开发者带来的影响。

Saron Yitbarek

Ingrid Burrington 想要走进云的世界。不是真实的“一朵云”哟,而是“云计算”的世界。

Ingrid Burrington

我不知道互联网真正的样子,我也不认为互联网是我想象中的那样,所以我想尝试找出它真实的模样。

00:00:30 - Saron Yitbarek

Ingrid 是一名记者。在她为《 大西洋 Atlantic 》撰写的系列报道中,她讲述了自己参观一个数据中心的经历,一个我们网络生活越来越依赖的地方。她在那里并不是走马观花逛一圈,而是浸入式的复杂体验。首先,她要拍照登记,申请访客身份卡。然后,要通过安检站,签署一份保密协议。最后,她才能去看机房。机房基本上就像个仓库,就像超市的那样,但比它大得多。

00:01:00

整个机房看起来有种别样的美,所有的东西都整齐陈列着。一堆光鲜靓丽的服务器上,连接着通往世界各地的光缆,光缆沿着天花板上的轨道整齐布线。正在通讯的光电信号闪烁着点点神秘的蓝光,仿佛粒子加速器一样。但本质上,它是一排排如猛兽般动力强劲的服务器。

00:01:30

数据中心每年消耗的能源比整个英国还要多。这就意味着它会释放惊人的巨大热量,这就是为什么当 Ingrid 环顾四周时……

Ingrid Burrington

对的,我发现这座建筑主要的设计理念,是建造最理想最完美的暖通系统(HAVC)。

00:02:00 - Saron Yitbarek

Ingrid 发现围绕数据中心的一切都强调经济实用,简单说就是一堆主机、一摞风扇、一大块租金便宜的地皮、用很多便宜的用来冷却的工业用水。完全没有“云”这个词本身散发的浪漫,但另一方面,我们的生活、我们的工作以及我们的话语,都在这个服务器的仓库里搏动着。

00:02:30 - Ingrid Burrington

是的,这有点超现实主义。并不是说我就知道那台机器里存有某人的电子邮件,这台机器又存有别的东西,而是我意识到周围有很多看不见的事情正在发生,我能听到服务器的呼呼声和大量运算产生的微小噪声。说来奇怪,我开始对工业充满敬畏……

00:03:00 - Saron Yitbarek

时刻要记住,在我们使用服务的时候,它们的基础,这些建筑都在某个隐蔽的角落嗡嗡运作着。以前,当我们谈论在云上存储东西,或创建应用程序时,我们有时会自欺欺人地认为它就像天上的云,是没有人能触碰的存在。但现实恰恰相反,一旦我们认识到云数据中心真实存在于某地,我们就会开始思考谁拥有着云了。那么是谁在控制这些服务器、线缆和风扇呢?

00:03:30 - Saron Yitbarek

它们是如何改变开发者构建未来的方式的呢?云让我们紧密地连接在一起。

我是 Saron Yitbarek,这里是《代码英雄》,一档由红帽公司推出的原创播客栏目,第六集,揭秘云计算。

Chris Watterston

没有所谓的“云”。那只是别人的电脑。

00:04:00 - Saron Yitbarek

Chris Watterston 是一名设计师,他对围绕云产生的误解很是恼火。这个词模糊了数据中心的形象,就像 Ingrid 参观过的那个一样。当 Chris 把这句口号做成贴纸时,他因此成为了网红。“没有所谓的‘云’,那只是别人的电脑。”这句话现在出现在 T 恤、帽衫、咖啡杯、海报、杯垫和许多主题演讲上。

00:04:30 - Chris Watterston

人们完全不理解云是什么,还用的很欢乐又心安。他们可能完全误解了云,不明白他们的数据实际上是穿过铜轴电缆、或者光纤,来到某一个实际上由他人管理和拥有的存储设备。显然,对于一些有需要隐藏的私人内容的人来说,这是相当可怕的隐患。

00:05:00

所以,下次你想把东西扔到云上的时候,想想 Chris 的贴纸吧。想想你到底要扔到哪里去。在 App 上工作也是同样道理,声称 App 跟服务器无关的说法都是骗人的,根本没有无服务器的 App。云就是别人的服务器、别人的电脑。不过云这件事情从某种意义上说,是一种成长。说到成长,在整一季节目里,我们会一直追溯开源的成长与变革。

00:05:30

从最初的自由软件运动到 Linux 的诞生,直至今天,开源工具和方法把我们带到了远离家园的云端。可以打个比方,一个人找房东租房,他需要签合同、搬进去、把房子整理成自己的居所。当开发者寻找云供应商时,他们也在做着同样的事情。这就是我们现在所处的情况,全世界的开发者都在转向各种云上线产品,然后开始明白租赁的真实含义。

00:06:00

严肃地发问一句,为什么我们一开始就急着跳上云端呢?

Brandon Butler

因为开发者不想维护 App 运行所需的设备。

Saron Yitbarek

这位是 Brandon Butler,《 网络世界 Network World 》的高级编辑,多年来致力于研究云计算。

00:06:30 - Brandon Butler

开发者想要开发 App,部署 App,并只在乎 App 能不能正常运行。我们已经看到云孕育的,越来越多的服务,例如无服务器计算、功能即服务、容器和容器管理平台,如 Kubernetes。

Saron Yitbarek

顺便打个广告,想了解容器和 Kubernetes,请看我们的上期节目

Brandon Butler

所有的这些成果都有助于抽象化 App 运行时所需要的底层基础设施。这将是一个可以在未来可预见的持续发展的趋势。

00:07:00 - Saron Yitbarek

云拥有巨大吸引力的部分原因,可以用“超大规模”这个词来解释。通过云为你提供的所有基础设施,你可以根据自己的需求,快速创建和定制自己的服务器。你不再需要购买自己的设备,你只需要租赁你想要的规模的云。Brandon 解释了“超大规模”对初创公司的具体帮助。

00:07:30 - Brandon Butler

使用公有云进行 App 开发的整套模型,对开发者来说是一个巨大的进步。它曾经成就了一系列全新的初创公司,这些初创公司也已经成长为大众都喜欢投资的公司。想想 Netflix 这样的公司,它的大部分后端都运行在亚马逊的以及其他的云上。

00:08:00 - Brandon Butler

这些公司现在如此壮大的原因,正是因为他们在使用云。因此,云对开发者的影响是不可轻视的。云已经成为过去十年,App 开发领域的主要转变。

Saron Yitbarek

Nick Bash 是 Meadowbrook 保险公司的一位系统管理员,他还记得在云计算诞生之前,调整基础设施是多么痛苦的事。

00:08:30 - Nick Bush

以前,有些人想出新项目的点子,我们会说,“这需要硬件支持,而我们没有。”他们会问,“那么我们该怎么办?”我们以前总是受到内存的限制,尤其是运行虚拟机软件,通常是最困难的部分。我们经常需要在任意时间启动虚拟机,但能随时启动的虚拟机数量总是不多。所以我们不得不花很多钱买新处理器、新内存、新硬件,或者花 5000 美元加新的东西。一旦我们从几个不同的供应商得到报价,就得报给管理层,他们要花时间审核。这样,仅仅是购买硬件都需要漫长的过程。

00:09:00

更不要说构建虚拟机,再反复考虑和测试等等。所以其实我的意思是,有了云,我们可以在几个小时内完成以往需要几个月完成的前期工作。让虚拟机运行起来,第二天就交付给其他人。所以这是一个很大的转变。

00:09:30 - Saron Yitbarek

在拓展性、速度和价格这些方面,云计算相当吸引人。还是拿租房作比喻,云就像可以让你免费得到一个管家和司机的服务,你很难对云计算说不。如今市场上有主要的四家壮志雄心的云供应商在开疆拓土。他们都想成为你在云上的“新房东”。但是且慢,每个租过房子的人都知道,租房和买房不一样。你不能自己拆掉一堵墙,或者安装一个新的按摩浴缸,你得通过房东来干这些事。

00:10:00

那么 Brandon Butler 先生,我们使用私有云,在某种程度上会受制于一家独资公司。这会不会对我们不利?

00:10:30 - Brandon Butler

当你使用云供应商的私有云时,你有不同的使用方法:你可以拥抱开源标准和开源平台,并且在云上运行开源软件,即便这是个私有云;你也可以使用不是开源的原生工具,这些工具可能在公有云上有更好的集成。因此,这是终端用户必须考虑的一个重大问题:我是想使用云供应商的原生工具,这些工具可能与这个云供应商提供的服务,以及其他服务更好的集成;还是拥抱开源标准,使用一个开源平台,享受更大的自由度,在自己和其他提供商的平台上也能运行?

00:11:00 - Saron Yitbarek

随着我们所依赖的云技术不断发展,四大云供应商相互竞争,我们作为开发者有了新选择。我们是放弃一些独立性,依靠单一的云供应商来保护我们的工作,还是选择另一条路,在保持独立性的同时最大化云的拓展性?

00:11:30

换句话说,我们能否在租房合同上写明,“房客有权任意处置该房 ,例如拆墙或其他装修”?

00:12:00

那么,放弃一点点独立性又有什么问题呢?如果你是一名开发者,可能没受到什么影响。因为大多数时候都有运维团队在背后监督开发者们小心行事,他们格外留心于具体细节。这位是 Major Hayden,他是 Rackspace 的首席架构师。

00:12:30 - Major Hayden

有些时候,开发者经常发现他们有各种不同的需要,比如某些专门化的存储,或者可能想要一定大小的虚拟机,或者想要一种云供应商未能提供的东西。还有一些东西可能开发者没有第一时间想要,但你认为他们需要的,对这些东西你还要进行成本效益分析。好吧,虽然使用公有云我们有很大的灵活性,但我们到底付出了什么代价?

Saron Yitbarek

Major 指出了另一个问题,这个问题超越了实用性,并且触及了像我这样的开发人员所信奉的核心,那就是开源实践。即使云供应商允许你使用自己的开源工具,但云本身并不是开源的

00:13:00 - Major Hayden

因此,开源对于云来说是一个需要处理的有趣议题,因为有大量的开源技术支持用户去高效地利用公有云,但并不是所有公有云都把它们的基础设施开源了。举个例子,如果你使用亚马逊,你无法知道他们使用的什么技术来构建虚拟机和其他服务。所以,你不可能对这些东西做点调整,或者很难了解幕后的机理和运作方法。

00:13:30 - Saron Yitbarek

如果你听过我们之前关于 DevOps 的节目,你会知道打破开发者和运维之间的壁垒会让我们获益良多。架构师 Major 刚给了我们一些真知灼见,接下来的这位是系统管理员 Nick Bush,他所在的团队正准备向云端迁移。开发者们已经厌倦了每五年一次硬件换代,每个人都喜欢尽可能快地扩展,而 Nick 想指出一些开发者可能没有考虑到的东西。

00:14:00 - Nick Bush

是的。我想说的是,云是存在延迟的。举个例子,就像远在蒙大拿的数据库服务器,对比我在街上用着 10-gig 的网络,本地数据库调用还是会花费更长的时间。要达到低延迟的云内数据库调用还有很长的路要走,还有其他的安全问题,因此我们暂时不需要担心物理上的前提。在本地,我们尚可以控制我们的硬件和其他类似的东西。一旦你进入了云端,你就得考虑连接问题。

00:14:30

我认为,你也得稍微担心一下安全问题,虽然这更多也是一个成本问题。你想要按月租一个云端虚拟机,要求网速快并且带有充足的存储空间。每千兆的传输和存储都是要花钱的,以前我们都是一次性买断一个机器,我们只要买好了一个云端虚拟机,就可以存储和使用。只要余额和储存空间都还足够,我们就不用付更多钱。

00:15:00 - Saron Yitbarek

声明一下,Nick 确实认为此事利大于弊。他只是不想让我们认为这是个完美的系统。如果你的云供应商宕机,而你想在其他云中重新部署应用程序,会发生什么情况?或者,如果在不同事务上使用不同的云能带来价格优势呢?运维人员提出的这些问题都可以被总结于一个词汇下,也就是 供应商依赖 vender lock-in 。你可能很熟悉这个词。

00:15:30

供应商锁定的意思是,在别人的服务器上构建业务会让你越来越依赖于他们的平台。你被绑定在这个平台了。可能突然之间,你被迫升级系统、付出更多成本、接受新限制,你变得身不由己。你懂的。

00:16:00

当我们都戴上 DevOps 的帽子时,我们开发者和运维就可以一起工作,面对供应商锁定,对症下药,但当我们沉浸在自己的代码中时,我们有时会忘记观览全局。为什么不找个折中方法,同时在公共和私有云上工作呢?终极解决方案可能是混合云,对于两方而言这都是最佳选择。我给 Bridget Kromhout 打了电话,询问她的看法。她是微软员工中的头号云开发提倡者,对这方面非常了解。

00:16:30

如果我们考虑一种混合的解决方案,既包含一些公有云,也包含一些私有云,这是两者之间的完美平衡吗?对于开发者,这是理想的解决方案吗?如果云是混合的,那么我就能想做什么就做什么,想用什么工具就用什么,同时仍然可以从大型公有云提供商那里获得一些好处。

00:17:00 - Bridget Kromhout

当然是的。举个例子,我有朋友在制造业中从事高性能计算研究工作,他们有各种各样的绝密资料,像 NDA 这样的东西,不适合放在公有云上。于是,他们可能会在自己的数据中心跟这些资料打交道,处理客户数据,或者研究数据,等等,也可能有其他的……

00:17:30

他们也有适合放在公有云上的其他工作资料,不过我想这个问题就……有时也会有这样的问题,公有云是否适合某些工作资料,比如,如果你计划使用 InfiniBand 同步你的不同笔记,你能在公有云中做到什么程度呢?

Saron Yitbarek

但这并不一定是完美的解决方案。Bridget 认为混合云也有自身的弊端。

00:18:00 - Bridget Kromhout

混合云的问题在于,有时,人们欺骗自己,认为他们可以接受一些实际上不工作的东西,所以如果他们之前等待两周来获得一个虚拟机,如果有人经历过一个完整的这样的情况,并且这个虚拟机还不能正常工作的话,就会有一堆的人由于失望而开始和他们的公有云提供商谈论信用卡问题了,然后他们会试着把这些东西粘合在一起,但是还是有数据来源和延迟的问题,我不是很确定,脱同步的数据集有很多出错的方式。我认为,如果你和云服务提供商合作,你可以有一些可用的直接沟通这样你就可以更好地同步数据,这样是很有帮助的。

00:18:30 - Saron Yitbarek

是的。当我们在开源的语境下谈到云的时候,我觉得,作为开发者,可能大多数人,都喜欢开源;如果你还在听我们的播客节目,就更是这样。对吧?你希望一切都是开放的,透明的,还向大众共享代码;但我觉得,当我们谈到云计算,因为它不会给人感觉是代码库,不会让人觉得云本身是个项目,它是环境,是可以用来帮助我们运行代码的东西,开发人员们还会坚持要让它像是传统的项目和代码库一样开源、透明吗?

Bridget Kromhout

我觉得这是一个非常合理的问题,我觉得这可能也会归结到你到底要注目于技术栈的哪一部分。想一想,你对芯片的了解有多少?你又能在何种程度上操控它们?

Saron Yitbarek

是的,这是真的。你说得不错。

Bridget Kromhout

他们坐在那里,他们有硅,他们也有秘密。他们不一定会将后者给你。

00:19:30 - Saron Yitbarek

是啊,硅和秘密。顺便说一句,这是个好播客的名字。

Bridget Kromhout

对吧?也许问题不在于是否一切都是开放的,而在于你需要开放的一切是否都是开放的,以及,当服务没有完全按照正确的方式运行时,你的服务提供者是否会对你保持信息透明,因为不该出的错误就是不该出。

00:20:00 - Saron Yitbarek

所以,我得到了 Bridget 作为一个公有云提供商的观点,她提出了一个有趣的观点。开发者在云上的控制需要多细?至于我,我的看法不一样。我不想为了一点公有云的优势而牺牲的是什么呢?比如说,一个应用在公有云上运行,然后,等一下,现在我已经扩大了规模,或者有新的合规要求,我的应用在私有云上更合适。

00:20:30

把应用从一个地方迁移到另一个地方之前,我需要知道它在迁移之后仍能工作。我需要知道它是以原先同样的方式打包,以同样的方式配置。换句话说,我需要知道从一个云跳到另一个云总是可能的。

除此之外,我们还有什么选择?仅仅锁定在一家云提供商?一个甚至可能完全垄断整个行业的供应商?不能选择迁移到另一个环境的话,这就像把一只手绑在背后写代码一样。

00:21:00

所以,我们不想欠下任何一朵云的人情,并且被它困住。我们希望在合适的时候能够在云间跳转。用摇滚传奇 皇后乐队 Queen 的名言来说,“我想要挣脱束缚”。我们希望能够获得公有云的绝佳拓展性,但又不放弃使用开源工具和方法所带来的自由。

00:21:30

有个好消息。混合云的建设正在顺利进行中。Mike Ferris,红帽公司的的业务架构副总裁,他给出了一个很好的解释,说明了混合云是如何帮助我们保持开源精神的。

00:22:00 - Mike Ferris

开源是世界上几乎每一个云服务的基础,现在即便不是大多数,也有许多世界上应用程序的基础设施和工具是从这里发展出来的,管理能力,以及人们用于构建、部署应用程序(无论是任务关键型,还是非任务关键型应用程序)的工具都是基于开源的。

00:22:30

混合云的概念和这一点非常兼容,这代表着,我们可以在混合云中处处使用开源工具,也可以最大程度地发挥出基础设施的优势。这是基于以下的一点事实:开源通过其在当今的强大影响力,能够在一定程度上定义下一代的开发模式。

Saron Yitbarek

我认为云计算本身具有开放的意愿。在本季节目中,我们花了很多时间讨论开源的起源。你甚至可以证明,某些版本的混合云是这些相同理想的延伸。

00:23:00 - Mike Ferris

在过去几十年里,开源开发活动的变化是越来越多的人参与进来了,包括像微软、IBM 这样的行业巨头。你知道,举个大公司的例子,他们要么使用开源软件来提供产品,要么构建开源软件并将其回馈给社区,或者两项都参与。

00:23:30

这些来自客户的重要需求通过那些大公司涌入,确实帮助了开源世界的发展,使之从最初设想中 Solaris 和 UNIX 的替代方案,发展为不仅是社区和业余爱好者使用,而且肯定也是部分任务关键型企业使用的基础。

00:24:00 - Saron Yitbarek

开源正在快速成长。现在,我们有机会确保我们记住我们从哪里来。当我们跃上云时,我们可以为自己声明开源的部分,以此来保持云的开放。幸运的是,由于有了 OpenStack® 平台这样的工具,在云之间构建开源桥梁变得更加容易了。Rackspace 的首席架构师 Major Hayden 描述了它的起源。

00:24:30 - Major Hayden

OpenStack® 来自于 Rackspace 和 NASA 的合作:“你看,这是一种构建基础设施的新方式,我们应该公开进行。我们应该得到更多的投入,应该和更多的人交流。我们应该得到更多的用例。” OpenStack® 是一组应用,它能很好地协同创建基础设施,并全面管理基础设施。无论你需要复杂的虚拟机、复杂的网络,还是有奇怪的存储要求,OpenStack® 通常可以满足大部分的要求。

Saron Yitbarek

Major 指的是,加入一些开源知道如何提供的东西:也就是适应性。

00:25:00 - Major Hayden

在我看来,OpenStack® 是一组相互连接的开放源码应用程序,它允许你构建你想要的基础设施。如果它不能建立你想要的,那么你可以进入社区,对它做出改变。我喜欢我去和顾客交谈时他们的反应,他们说,“我们想改变这个。我们想要改变这一切。”我们会说,“嗯,你可以。”

Saron Yitbarek

我们如何确保这样的的适应性被包含在明天的云中呢?就像我们在之前的节目中谈到的许多问题一样,这需要强大的社区。有请 Brandon Butler,《网络世界》的高级编辑。

00:25:30 - Brandon Butler

例如,我们已经看到了云原生计算基金会的成立,这个基金会制定标准,推广应用容器的使用,并创造了 Kubernetes。我们也看到了 OpenStack 基金会的成立,好将 OpenStack® 用户聚集在一起,讨论创建开源基础设施服务云时的最佳实践。

00:26:00

支撑这些开源社区的社群对于开发下一波开源工具,学习如何最好地使用这些开源平台的,以及鼓励公有云厂商接受这些开源标准都非常重要。

Saron Yitbarek

一旦我们开始构建混合云,并使其尽可能地开放,潜力似乎真的无穷无尽。Major,请说。

00:26:30 - Major Hayden

最让我兴奋的是看到更多的东西可以聚集在不同的云之上。例如,OpenStack® 提供了一个很好的基础设施基础层,但是你可以在它之上做很多事情。我想有时候不同的公司会采用 OpenStack®,然后说:“伙计,我现在该怎么办?我的自由程度太高了。我不知道该怎么办。”这就像你有一个装满食物的冰箱,你会想,“啊,我不知道该做什么菜。”

00:27:00 - Saron Yitbarek

我喜欢这个问题。Chris Watterson 告诉我们的可能是对的。

Chris Watterston

没有所谓的“云”,那只是别人的电脑。

00:27:30 - Saron Yitbarek

但故事并未在此结束。我们要与混合云一起跨入下一章。创建混合云应用的关键可能还没有被破解。跨多云管理任务,对于今天的代码英雄们来说将是一项艰巨的任务。会有很多尝试和错误,但这是值得的,因为我们知道的唯一的一件事是,保持开源意味着开发人员总是可以构建他们想要工作的世界。这种灵活性正是紧紧抓住开源最擅长的叛逆精神的诀窍。

00:28:00

下一集是我们本季的最后一集,我们将以一种让你惊讶的方式,从宏观角度来看开源作为一种全球现象是什么样的。我们也将展望开源的未来,我们的开发人员如何保持像 Linus Torvalds 这样的英雄的精神,即使当他们正在重塑他们的行业时。

00:28:30

《代码英雄》是一档红帽公司推出的原创播客。想了解更多关于本期和往期节目的信息,请访问 RedHat.com/CommandLineHeroes 。在那里你也可以注册我们的新闻通讯。想免费获得新一期节目推送,请务必订阅我们。只要在苹果播客、Spotify、Google Play、CastBox 和其他播客平台中搜索《代码英雄》,然后点击订阅,你就可以第一时间收听新一期。我是 Saron Yitbarek。感谢你的聆听,编程不止。

OpenStack® 和 OpenStack 标志是 OpenStack 基金会在美国和其他国家的注册商标/服务标志或商标/服务标志,并经 OpenStack 基金会许可使用。我们不是 OpenStack 基金会或 OpenStack 社区的附属机构,也没有得到 OpenStack 基金会或 OpenStack 社区的认可或赞助。

什么是 LCTT SIG 和 LCTT LCRH SIG

LCTT SIG 是 LCTT 特别兴趣小组 Special Interest Group ,LCTT SIG 是针对特定领域、特定内容的翻译小组,翻译组成员将遵循 LCTT 流程和规范,参与翻译,并获得相应的奖励。LCRH SIG 是 LCTT 联合红帽(Red Hat)发起的 SIG,当前专注任务是《代码英雄》系列播客的脚本汉化,已有数十位贡献者加入。敬请每周三、周五期待经过我们精心翻译、校对和发布的译文。

欢迎加入 LCRH SIG 一同参与贡献,并领取红帽(Red Hat)和我们联合颁发的专属贡献者证书。


via: https://www.redhat.com/en/command-line-heroes/season-1/crack-the-cloud-open

作者:Red Hat 选题:bestony 译者:LikChung 校对:acyanbird

本文由 LCRH 原创编译,Linux中国 荣誉推出