第 8 页的文章列表

分布式文件上传方案

-## 一、背景 考虑可扩展性,后台的服务肯定是要能够支持任意的扩展的,这样才能在业务量增长时通过增加机器的方式来应对。这对后台服务提出了一个要求,必须处理好分布式环境和单机环境的不同带来的问题。比如:在文件的分片上传这一场景下,应该负载均衡的问题,一个文件的多个分片请求会分布到不同的服务器上,这导致在将多个分片合并成完整文件时出现问题,而单机情况下则完全不会有这样的问题。

二、难点

这个问题的解决方案有很多种,但是需要根据实际情况尽量选择简洁、易部署和维护的方案进行,并且不能丢掉分布式系统的优点。比如网上的有的方案是使用单独的文件上传服务器,但是这就变成了单机服务了。也有使用NFS挂载的方案,即所有的服务器挂载一个相同的NFS目录,所有上传相关的文件都存放在挂载的目录下,这不仅给运维带来了麻烦,为了保证NFS的高可用,也带来了额外的运维成本。

三、解决方案

3.1 借助负载均衡

借助负载均衡的方案很简单,这是和应用无关的一种方法。即通过负载均衡这一层,将同一个文件的不同分片的请求全部导向到同一个服务器上。比如负载均衡这一块使用的是nginx, 通过url hash的方式来完成,可以使用如下的配置:

upstream backend {
    server 0.0.0.0:8080;
    server 0.0.0.0:8081;
    server 0.0.0.0:8082;
    hash $request_uri;
}
server {
    location / {
        proxy_pass http://backend;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header Host $host;
    }
}

然后写一个简单的服务来验证一下:

func post(resp http.ResponseWriter, req *http.Request) {
    v := req.PostFormValue("key")
    identity := req.PostFormValue("identity")
    log.Printf("identity: %v, value: %v\n", identity, v)

    resp.WriteHeader(http.StatusOK)
    _, err := resp.Write([]byte("ok"))
    if err != nil {
        log.Println("error:", err)
    }
}

func main() {
    var addr string
    flag.StringVar(&addr, "addr", "0.0.0.0:8080", "http listen addr:port")
    flag.Parse()

    http.HandleFunc("/Post", post)
    log.Println("listen ", addr)
    err := http.ListenAndServe(addr, nil)
    if err != nil {
        log.Fatal(err)
    }
}

我们启动三个服务:

./godemo -addr 0.0.0.0:8080
./godemo -addr 0.0.0.0:8081
./godemo -addr 0.0.0.0:8082

使用curl来post数据过来,请求的地址类似于: http://0.0.0.0/Post?123456?后面可以当成是文件的唯一标识码,比如文件和用户id的组合的md5信息等,这样对于同一个用户上传的同一个文件的不同分片请求,通过url哈希都会得到同样的结果,这样就会被转发到同一个后端服务器。

curl http://0.0.0.0/Post\?123456 -d "key=value&identity=123456" -X POST
curl http://0.0.0.0/Post\?123456 -d "key=value&identity=123456" -X POST
curl http://0.0.0.0/Post\?123456 -d "key=value&identity=123456" -X POST
curl http://0.0.0.0/Post\?789abc -d "key=vvvvv&identity=789abc" -X POST
curl http://0.0.0.0/Post\?789abc -d "key=vvvvv&identity=789abc" -X POST
curl http://0.0.0.0/Post\?789abc -d "key=vvvvv&identity=789abc" -X POST

结果如下:

端口8080的服务收到了三条请求:
2019/09/26 13:58:52 identity: 123456, value: value
2019/09/26 13:58:56 identity: 123456, value: value
2019/09/26 13:59:03 identity: 123456, value: value

端口8081的服务收到了三条请求:
2019/09/26 13:59:40 identity: 789abc, value: vvvvv
2019/09/26 13:59:43 identity: 789abc, value: vvvvv
2019/09/26 13:59:44 identity: 789abc, value: vvvvv

如果在k8s集群中部署,使用nginx-ingress的话可以使用以下的部署方案:

apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
    name: pipeline-ingress
    namespace: default
    annotations:
      nginx.ingress.kubernetes.io/proxy-body-size: "50m"
      nginx.ingress.kubernetes.io/upstream-hash-by: "$request_uri"
spec:
    rules:
        - host: pipeline.dev.com
          http:
              paths:
                  - path: / 
                    backend:
                        serviceName: pipeline-service
                        servicePort: 8888

当然这种方式需要注意你的每个请求的URI都需要加上额外的参数(比如用户的userId的md5),这样才能均匀的分布到不同的服务器上。

3.2 后台程序自动proxy请求

这个方案的思路是集群中的每个服务实例都有单独的标识,文件分片在第一次上传时会返回给它一个该请求所属服务器的identity,之后所有的请求会带上这个identity,之后收到请求的服务器会检查这个identity是不是属于自己,如果不属于自己,则把这个请求转发给所属的服务器。 这个方案要求每台服务器都知道其他所有服务器的identity和地址。这里我们可以使用etcd这样的分布式数据库来存储。每台服务器在启动时都向etcd里注册自己的identity和address,之后服务器转发的时候都向etcd里面查找对应的address即可。 下面是一个示例的代码:

package main

import (
    "context"
    "encoding/json"
    "flag"
    "io/ioutil"
    "log"
    "net/http"
    "go.etcd.io/etcd/clientv3"
    "net/url"
    "time"
)

type Server struct {
    Etcd string
    Addr string
    Identify string
}

type ResponseObj struct {
    Identity string `json:"identity,omitempty"`
    Value string `json:"value"`
}

func getEtcdKV(etcd string) (clientv3.KV, error) {
    cfg := clientv3.Config{
        Endpoints:               []string{etcd},
        // set timeout per request to fail fast when the target endpoint is unavailable
        DialTimeout: time.Second,
    }

    cli, err := clientv3.New(cfg)

    if err != nil {
        return nil, err
    }

    return clientv3.NewKV(cli), nil
}

func httpProxy(anotherServer string, body map[string]string) (*http.Response, error) {
    formData := url.Values{}

    for k, v := range body {
        formData.Set(k ,v)
    }

    return http.PostForm(anotherServer, formData)
}

func (s *Server) Register() error {
    cli, err := getEtcdKV(s.Etcd)
    if err != nil {
        return err
    }

    ctx, cancel := context.WithTimeout(context.Background(), time.Duration(1)*time.Second)
    _, err = cli.Put(ctx, s.Identify, "http://" + s.Addr)
    cancel()
    if err != nil {
        return err
    }

    return nil
}

func (s *Server) Post(resp http.ResponseWriter, req *http.Request) {
    v := req.PostFormValue("key")
    identity := req.PostFormValue("identity")
    log.Printf("identity: %v, value: %v\n", identity, v)

    var data ResponseObj

    if identity != "" && identity != s.Identify {
        // proxy
        cli, err := getEtcdKV(s.Etcd)
        if err != nil {
            log.Println("get kv client error", err)
            resp.WriteHeader(http.StatusInternalServerError)
            resp.Write([]byte("error"))
            return
        }

        etcdResp, err := cli.Get(context.Background(), identity)
        if err != nil {
            log.Println("get value error: ", err)
            resp.WriteHeader(http.StatusInternalServerError)
            resp.Write([]byte("error"))
            return
        }

        if len(etcdResp.Kvs) == 0 {
            log.Println("没有值")
            resp.WriteHeader(http.StatusInternalServerError)
            resp.Write([]byte("error"))
            return
        }

        proxyAddr := string(etcdResp.Kvs[0].Value)

        log.Println("proxy to: ", proxyAddr)

        text := map[string]string {
            "key": v,
            "identity": identity,
        }

        proxyResp, err := httpProxy(proxyAddr+"/Post", text)
        if err != nil || proxyResp.Body == nil {
            log.Println("http proxy error: ", err)
            resp.WriteHeader(http.StatusInternalServerError)
            resp.Write([]byte("error"))
            return
        }

        bodyData, err := ioutil.ReadAll(proxyResp.Body)
        if err != nil {
            log.Println("read proxy body error: ", err)
            resp.WriteHeader(http.StatusInternalServerError)
            resp.Write([]byte("error"))
            return
        }

        err = json.Unmarshal(bodyData, &data)
        if err != nil {
            log.Println("json unmarshal error: ", err)
            resp.WriteHeader(http.StatusInternalServerError)
            resp.Write([]byte("error"))
            return
        }
    } else {
        data.Identity = s.Identify
        data.Value = v
    }

    text, err := json.Marshal(data)
    if err != nil {
        log.Println("marshal json error: ", err)
        resp.WriteHeader(http.StatusInternalServerError)
        return
    }

    resp.WriteHeader(http.StatusOK)
    _, err = resp.Write(text)
    if err != nil {
        log.Println("error:", err)
    }
}

func main() {
    var addr string
    var identity string
    var etcd string
    flag.StringVar(&addr, "addr", "0.0.0.0:8080", "http listen addr:port")
    flag.StringVar(&identity, "identity", "", "identify the server")
    flag.StringVar(&etcd, "etcd", "http://127.0.0.1:2379", "etcd server url")
    flag.Parse()

    if identity == "" {
        log.Fatal("identity不可为空")
    }

    var server = &Server{
        Addr:     addr,
        Identify: identity,
        Etcd: etcd,
    }

    err := server.Register()
    if err != nil {
        log.Fatal("register server error, check your etcd server: ", err)
    }

    http.HandleFunc("/Post", server.Post)
    log.Println("listen ", addr)
    err = http.ListenAndServe(addr, nil)
    if err != nil {
        log.Fatal(err)
    }
}

3.3 基于S3存储的方案

兼容S3的存储系统有一个对外的接口叫做: ComposeObject。这个接口可以将多个文件合并成一个文件存储到指定位置。如果我们的底层存储是基于兼容S3的系统,那么就可以利用这个接口轻松的实现。 方案的步骤可以描述成以下:

  1. 前端实现将文件分片,然后上传
  2. 上传的请求会因为前端负载均衡的原因分布到不同的服务器,每个上传请求都要带上这个文件、用户id、随机字符串组合的md5值,服务器收到请求后,只管将分片上传到以md5值为名的目录下,一旦上传成功,使用etcd或redis这样作为分片计数器+1.
  3. 分片总数等于总分片数时调用ComposeObject组合所有分片存储到指定位置即可。
分类:分布式系统

ceph架构研究

这篇文章的绝大多数内容都是官网原文的翻译:Ceph Architecture

ceph是什么

ceph是一个分布式的文件存储系统,它提供了以下三种存储能力:

  • 块设备存储(block device)
  • 文件系统存储(filesystem)
  • 对象存储(object storage)

因此如果你需要多种存储方案的话,ceph是一个非常好的选择。这几种存储方案的差别是什么呢? 块设备存储是将底层的存储能力以逻辑硬盘的方式暴露给主机使用。主机只感知到单块物理硬盘的挂载,底层的复杂机制都被屏蔽 对象存储是将文件抽象成对象,然后通过网络接口(比如s3的api)来对文件进行操作(put,get,delete等)。 文件系统存储是像FTP,NFS这种,可以将ceph的存储能力当成普通的文件系统来使用,也支持cd, ls这样的文件系统命令。 ceph filesystem

ceph 架构一览

ceph arch 通过上图可以看出,ceph的底层核心是RADOS,然后通过LIBRADOSCEPHFS以及基于LIBRADOS的RADOSGW, RBD对外提供存储功能。

ceph 存储集群

ceph基于RADOS提供了无限的可扩展的存储集群。关于RADOS的知识可以看这篇论文: RADOS - A Scalable, Reliable Storage Service for Petabyte-scale Storage Clusters 一个Ceph存储集群包括两类常驻服务:

  • Ceph Monitor
  • Ceph OSD Daemon

ceph daemons Ceph Monitor维护了一个关于集群信息映射的主副本。Ceph monitors集群保证了在一个monitor服务停止时的高可用性。存储集群客户端从Ceph Monitor获取一份集群信息映射的拷贝。 Ceph OSD服务负责检查自身的状态以及其他OSD的状态,然后汇报给monitors。 存储集群客户端和每一个Ceph OSD服务使用CRUSH算法来高效的计算数据位置的信息,而不是依赖于一个中心化的查找表。librados是一个很重要的角色,它提供了Ceph的高层特性,一系列的服务接口都是建设在librados上层的。

存储数据

Ceph存储集群从Ceph客户端接受数据,这里的客户端可以是Ceph块设备存储,对象存储,文件系统或者是基于librados的自定义实现。librados将数据作为一个对象存储。每一个对象都在文件系统中对应一个文件,这些文件存储在对象存储设备上。Ceph OSD服务在存储磁盘上处理这些读写操作。 osd handle read/write operations Ceph OSD服务在一个扁平的命名空间中(没有目录结构)存储所有的数据,每个数据都作为一个对象。一个对象有唯一标识符,二进制数据,以及有一系列键值对的元数据。这完全取决于Ceph客户端的实现。举例来说,CephFS使用元数据来存储文件属性,比如文件拥有者,创建日期,最后修改日期等等。 ceph object

注意: 对象ID是在整个集群中唯一的,而不仅仅是本地文件系统。

扩展性和高可用

在传统架构中,客户端和一个中心化的组件(比如gateway, broker, API, facade等)通信。这个中心化组件在一个复杂系统中扮演了一个单点的入口。这在性能和扩展性上都导致了局限性————单点失败。(比如,如果中心组件宕机,整个集群都不可用)。 Ceph消灭了这个中心化的入口,让客户端直接和Ceph OSD服务通信。Ceph OSD服务在其他Ceph节点上创建对象副本来保证数据安全和高可用。Ceph同样使用一个monitor的集群来保证高可用。为了消灭中心化,Ceph使用了一个叫做CRUSH的算法。

CRUSH介绍

Ceph客户端和Ceph OSD服务都使用CRUSH算法来高效的计算对象存储位置信息,而不是必须取决于一个中心化的查找表。相比于之前的方案,CRUSH提供了一个更好的数据管理机制,通过清晰的将工作分布到集群中的所有客户端和OSD服务,可以达到很好的扩展性。CRUSH使用智能的数据复制来保证灵活性,这更适用于超大规模的存储。

集群映射表

Ceph依赖于Ceph客户端和OSD服务,它们包含了5个映射表。

  • 监控映射表: 包含集群的fsid, 位置, 名字地址以及每个monitor的端口。它同样记录了当前的代数,映射表的创建时间,最后一次修改时间。可以使用ceph mon dump来查看一个监控表

  • OSD映射表: 包含集群的fsid, 这个映射表的创建时间和最后修改时间,pools的列表,副本数量,PG数量,OSD的列表以及他们的状态。使用ceph osd dump来查看osd表。

  • PG映射表: 包含PG版本号, 时间戳,最新的OSD映射表的代数,全比率,每一个放置组的详情,比如PG ID, Up Set, Acting Set,以及PG的状态(比如active + clean),每一个pool的数据使用统计信息。

  • CRUSH映射表: 包含存储设备的列表,失败域的层级(比如device,host,rack,row,room等),存储数据时的遍历层级的规则。

  • MDS映射表: 包含当前的MDS映射表的代数, 映射表的创建时间,最后修改时间。它同样包含存储元数据的池,元数据服务器的列表,那些元数据服务器是up和in的。执行ceph fs dump来查看MDS映射表。

每一个映射表都包含它的操作状态改变的可迭代历史。Ceph Monitors维护一个集群映射表的主拷贝,包含了集群成员,状态,改变,以及Ceph存储集群的全局健康状态。

高可用的监控服务

在Ceph客户端可以读或写数据前,它们都必须和一个Ceph监控服务通信来获取最新的集群映射表的拷贝。一个Ceph存储集群可以只有一个Monitor角色。当然,这会导致单点失败。 为了增加可靠性和错误容忍,Ceph支持监控服务的集群。在一个监控服务的集群中,延迟和其他错误会导致一个或多个监控服务落后于集群的当前状态。因此,Ceph必须在各个监控服务实例之间就集群状态达成一致。Ceph总是相信大多数的监控服务。Paxos算法被用来就当前集群状态达成共识。

高可用的认证

为了鉴别用户以及防止中间人攻击,Ceph提供了cephx认证系统来认证用户和后台服务。

cephx协议不致力于传输过程中数据加密(比如SSL/TLS)或者是其他的加密。

Cephx使用共享的秘钥来认证,这意味着客户端和监控集群都有一份客户端秘钥的拷贝。认证协议需要双方都能证明给对方自己拥有这个秘钥的拷贝,但是又不能把秘钥原文说出来。这提供了两端的验证,意味着集群确信用户有这个秘钥,用户也确信集群有这个秘钥。 Ceph的一个关键的可扩展的特性是避免对Ceph object store的实现有中心化接口,这意味着Ceph客户端必须能够和OSD直接通信。为了保护数据,Ceph提供了cephx认证系统。cephx协议的机制和Kerberos类似。 用户调用Ceph客户端来和监控服务器通信。和Kerberos不同的是,每一个监控服务都可以验证用户以及分发私钥,所以在使用cephx时不再有单点失败或瓶颈了。监控服务返回一个认证数据结构,类似于Kerberos ticket,包含获取Ceph服务的会话秘钥。会话秘钥被用户永久秘钥加密过,因此只有用户可以向Ceph监控服务请求服务。之后客户端使用会话秘钥来请求它想要的服务,监控服务向客户端提供一个ticket,这个ticket可以向OSD认证客户端来处理数据。 Ceph Monitors和OSD共享秘钥,因此客户端可以使用监控服务提供的ticket和任意的OSD或元数据服务器通信。和Kerberos一样,cephx ticket会过期,因此攻击者不能使用过期的ticket或会话秘钥来获取服务。 为了使用cephx,管理员必须首先设置用户。在下图中,client.admin这个用户从命令行中调用ceph auth get-or-create-key来生成用户名和秘钥。Ceph的认证系统生成用户名和秘钥,存储一份拷贝给所有的监控服务,然后将用户秘钥传送给client.admin用户。这意味着客户端和监控服务共享同一个秘钥。 request create user 为了通过监控服务的认证,客户端把用户名发送给监控服务,监控服务生成一个会话钥匙,使用秘钥附加上用户名来加密。之后,监控服务把加密后的ticket返回给客户端。客户端使用共享的秘钥解密,获取到会话钥匙。会话钥匙为当前的会话提供认证。客户端之后请求由这个会话钥匙签名的ticket。监控服务生成ticket,使用用户秘钥加密并返回给客户端。客户端解密ticket,使用它来签名给OSD和元数据服务器的请求。 client to osd cephx协议认证每个在客户端机器和Ceph服务器之间的会话。每一个在客户端和服务器之间发送的信息,都会接连通过初始化认证,使用ticket签名,这个签名可以由监控服务,OSD和元数据服务器用他们共享的秘钥来验证。 complete process 由这种认证提供的保护是在Ceph客户端和Ceph服务端之间的。在Ceph客户端之外这个认证是不管用的。如果用户通过远程连接到Ceph客户端上,Ceph的认证不会应用到这个远程连接上。

智能的后台服务造就了超大规模的可扩展性

在很多集群架构中,集群成员的主要目的是通过一个中心化的接口知道哪些节点可以访问。然后中心化的接口通过二次转发来提供服务,这回导致在pb级别向eb级别扩展时有很大的瓶颈。 Ceph消灭了这个瓶颈:Ceph的OSD服务和Ceph客户端是集群感知的。比如Ceph客户端、Ceph OSD服务都知道集群中的其他Ceph OSD服务。这使得Ceph OSD服务可以直接和其他Ceph OSD服务以及Ceph监控服务通信。另外,它使得Ceph客户端可以直接和Ceph OSD服务直接通信。 这种方法也最大化的利用了节点的CPU和RAM, 带来了以下几个主要的好处:

  • OSD 为客户端直接提供服务:因为任何网络设备都有最大并发连接的瓶颈,一个中心化的系统在高扩展的情况下会出现一个很低的物理瓶颈。通过让Ceph客户端直接和Ceph OSD服务通信, Ceph同时增加了性能和系统容量,并且解决了单点失败的问题。Ceph客户端可以维护一个和Ceph OSD服务的会话,而不是一个中心化的系统,只要它们需要这样做。

  • OSD成员和状态:Ceph OSD服务加入到集群中并且汇报它们的状态。在最低级别上,Ceph OSD服务状态是up或者down的,代表着它是否能够处理Ceph客户端的请求。如果Ceph OSD服务是down的,这意味着Ceph OSD服务的异常。如果Ceph OSD服务不在运行(crash了),Ceph OSD服务就不能通知Ceph监控服务它停止运行了。OSD服务周期性的给Ceph监控服务发送消息,如果Ceph监控服务在约定的周期内没有收到OSD服务的消息,它就把这个OSD服务标记为down。这种机制叫做failsafe(failsafe可以理解可以故障的自动保护装置,当OSD出现故障会自动将其排除从而避免引发更大的故障)。当然,OSD服务也会查看它相邻的OSD服务是否停止,然后汇报给Ceph监控服务。这保证了Ceph监控服务是一个轻量级的进程(脏活累活都被OSD服务做了)。

  • 数据清理:作为维护数据一致性和清洁度的一部分,Ceph OSD服务可以在PG(placement group, 放置组)中清理对象。Ceph OSD服务可以将一个PG中的对象和存储在另一个OSD服务中的副本进行元数据的对比。清理操作(通常是每天)会找出bug或文件系统的错误。Ceph OSD服务同样会执行更深层次的清理,通过对对象执行按位的对比。深层次的清理(通常是每周)会找到硬盘上坏的扇区。

  • 数据复制:像Ceph客户端一样,Ceph OSD服务使用CRUSH算法,但是Ceph OSD服务使用它来计算对象的副本应该存储在哪里(或者是重新负载均衡)。在一个典型的写操作场景下,一个客户端使用CRUSH算法来计算对象存储在哪里,将这个对象映射到pool和PG中,然后查看CRUSH映射表来鉴别PG组的主OSD服务 客户端将对象写入到主OSD服务的PG组中。之后,主OSD携带着他自己的CRUSH映射表的拷贝存储到第二和第三个OSD服务中,这是为了多副本的备份。一旦它确认数据存储成功就返回给客户端。

osd write for replication 因为有这样的数据复制的能力,Ceph OSD服务使得Ceph客户端不需要负责这件事,同时也能保证高的数据可用性和数据安全。

动态集群管理

在高可用和可扩展性章节,我们解释了Ceph如何使用CRUSH、集群感知、智能服务扩展以及维护高可用。Ceph的关键设计就是自治、自我恢复、智能Ceph OSD服务。下面我们更深入的探究一下CRUSH如何在现代化的云存储架构中发挥作用,包括存储数据、集群的重新负载均衡以及动态的从错误中恢复。

关于pool

Ceph存储系统支持一个叫做pool的概念,这是存储对象的逻辑分区。 Ceph客户端从Ceph监控服务中获取集群映射表,然后向pool中写入对象。pool的大小或副本数量、CRUSH的规则以及PG的数量决定了Ceph如何存储数据。 how crush work pool至少会设置以下参数:

  • 对象的拥有者/访问权限
  • PG的数量
  • 使用的CRUSH规则

映射PG到OSD

每一个pool都有一系列的PG。CRUSH动态的将PG映射到OSD中。当Ceph客户端存储对象,CRUSH将会映射每一个对象到一个PG中。 将对象映射到PG创建了一个中间层,这个中间层位于Ceph OSD服务和Ceph客户端中间。在动态的存储对象时,Ceph存储系统必须能够增长(或收缩)以及重新负载均衡。如果Ceph客户端知道哪个Ceph OSD服务拥有哪一个对象,这就导致Ceph客户端和Ceph OSD服务紧耦合了。相反的,CRUSH算法将每一个对象映射到PG,然后将每一个PG映射到一个或多个OSD服务。这个中间层允许在有新的OSD服务以及新的底层OSD设备上线时,Ceph可以动态重新负载均衡。下面的图表演示了CRUSH如何映射对象到PG,然后映射PG到OSD。 crush mapping 客户端在有集群映射表的拷贝和CRUSH算法的情况下,可以准确的计算出在读取或写入对象时使用哪一个OSD。

计算PG ID

当一个Ceph客户端绑定到一个Ceph监控服务时,它会获取集群映射表的最新拷贝。有了这个集群映射表,客户端知道所有的监控服务,OSD,元数据服务。然而,它并不知道任何关于对象的位置信息。

对象位置需要计算

客户端唯一需要的输入是对象ID和pool。这很简单:Ceph在命名的pool中存储数据。当客户端存储一个命名的对象时,它使用对象名、一个哈希码、pool中PG的数量和pool名来计算PG。Ceph客户端使用以下步骤来计算PG ID。

  • 1.客户端输入pool名和对象ID(比如: pool=“liverpool”, object-id=“john”)

  • 2.Ceph获取对象ID,然后做一下哈希操作

  • 3.Ceph计算哈希值除PG数量的余数,(比如58)得到了PG ID

  • 4.Ceph获取给定的pool名的pool ID(比如“liverpool”=4)

  • 5.Ceph将pool ID放在PG ID之前(比如4.58)

在一个忙碌的会话中,计算对象位置要比执行对象位置查询快很多。CRUSH算法允许客户端计算对象应该存在哪里,这使得客户端可以直接和主OSD服务通信,存储或获取对象。

peering and sets

在之前的章节中,我们提到Ceph OSD服务检查彼此的心跳,然后汇报给Ceph监控服务。Ceph OSD做的另外一件事叫做“窥探(peering)”,这会让所有存储PG的OSD对PG中的对象状态达成一致。事实上,Ceph OSD服务也会汇报“peering”的失败给Ceph的监控服务。Peering的问题通常会自我解决。

对状态达成一致并不意味着PG有最新的内容

Ceph存储集群被设置成一个对象至少有两个副本(比如, size=2),这是数据安全的最小要求。为了高可用,Ceph存储集群应该存储多于两个的副本(比如size=3并且最小size=2)。这样它就能在维护数据安全的时候以一个降级状态继续运行。

重新负载均衡

当你添加一个Ceph OSD服务到集群中,集群映射表会随之更新。根据之前的计算PG ID的方法,这会改变集群映射表。于是,它改变了对象的位置,因为它改变了计算的一个输入参数。下面的图标描述了重新负载均衡的过程。有一些但不是所有的PG会从已存在的OSD(OSD1和OSD2)中迁移到新的OSD(OSD3)中(这个过程虽然看似很简单粗暴,但其实对大型系统影响很小)。即使在重新负载均衡的过程中,CRUSH仍然是稳定可靠的。大多数PG保留它们原有的配置,每一个OSD获取到更多的可用容量,所以在重新负载均衡之后新的OSD没有负载峰值。 ceph rebalance

数据一致性

作为维护数据一致性和清洁度的一部分,Ceph OSD同样可以在PG中清理对象。关于OSD如何维持各个副本的对象一致性,可以参考上面的数据清理。

纠删码(erasure coding)

一个纠删码pool将每个对象分成K+M个块。数据被分为K个数据块和M个编码块。这个pool被配置成K+M的大小,因此每个块都可以存在一个OSD上。块的排序作为对象的属性存储起来。 举例来说,一个使用5个OSD(K+M=5)的纠删码pool可以容忍2块数据的丢失(M=2)。

读写编码块

当一个叫做NYAN的对象包含“ABCDEFGHI”的内容被写入到pool中,擦除编码功能将内容分成3个数据块:第一部分是ABC,第二部分是DEF,第三部分是GHI。如果内容长度不是K的整数倍,会被填充内容。这个功能同样创建两个编码块:第4个是YXY,第5个是QGC。每一个块都被存储到一个OSD中。这些块以同样的名字(NYAN)以对象的形式存储在不同的OSD上,块的创建顺序必须保留,存储对象的属性中(shard_t),作为名字的额外补充。块1包含ABC,存储在OSD5中,块4包含YXY存储在OSD3中。 erasure conding 当对象NYAN从纠删码pool中读取时,解码功能读取三个块: 块1包含ABC,块3包含GHI,块4包含YXY。然后它重建了对象的原始内容ABCDEFGHI。这个解码功能被通知块2和块5是缺失的(它们被称作’擦除’)。块5不会被读取是因为OSD4从集群中退出了。一旦3个块被读取解码功能就会被调用,而此时OSD2因为最慢所以根本没有被考虑进来。 erasure decode

中断完整的写入

在一个纠删码pool中,主OSD接受所有的写操作。它负责编码内容为K+M块,然后将它们发送给其他的OSD。它同样负责维护PG日志的版本号。 在下面的图表中,一个纠删码的PG被创建成K=2 M=1,它支持3个OSD,两个存储K,一个存储M,分别称为OSD1、OSD2、OSD3。一个对象被编码以及存储在OSD中,块D1v1(数据块序号1,版本1)在OSD1上,块D2v1在OSD2上,C1v1(编码块序号1,版本1)在OSD3上。每一个OSD上的PG日志都是唯一的(1,1 是epoch 1. version 1) interrupted-fill-writes OSD1是主服务,接受客户端的完整的写入,这意味着写入的数据要完整的替换对象而不是覆盖一部分。版本2(v2)的对象被创建来覆盖版本1(v1)。OSD1将数据编码到3块: D1v2在OSD1上,D2v2在OSD2上,C1v2在OSD3上。每一个块都被发送到目标OSD上。当OSD接受到消息要写入数据块时,它同样创建一个新的PG日志来反映这个改变。举例来说,只要OSD3存储了C1V2,它添加1,2到日志中。因为OSD是异步工作的,当其他块已经存储好了(比如C1v1和D1v1)一些数据块可能还在处理(比如D2v2)。 write replace 如果一切顺利,数据块都会被存储,日志中的last_complete指针会从1,1移动到1,2 full write success 最后,之前的版本都可以被移除了: OSD1上的D1v1,OSD2上的D2v1, OSD3上的C1v1 remove previous version 但是如果发生了异常,OSD1在D2v2仍然写入的时候停止了,对象的版本2只是部分写入:OSD3有一个块但是不足以恢复。它丢失了两个块: D1v2和D2v2,但是纠删码参数K=2,M=1要求至少2个块是可用的,才能恢复第三个块。OSD4成为新的主服务,找到last_complete日志是1,1,这应该是最新的日志了。 osd4 online 日志1,2被发现在OSD3上,这和在OSD4上存储的最新版本1,1不一样、因此1,2被取消,C1v2块被移除。D1v1块被解码功能重建出来存在OSD4上。 rebuild-on-osd4

缓存分层

缓存层提供给Ceph客户端更好的IO性能,因为一部分数据是存储在缓存中的。缓存层包括创建一个快速/昂贵的存储设备的pool,被配置成缓存层,便宜的存储设备被当成存储层使用。Ceph objecter负责处理将对象放在哪里,tiering代理决定什么时候将对象从缓存中写入到后面的存储层。因此缓存层和背后的存储层对Ceph客户端是完全透明的。 cache tiering

ceph协议

Ceph客户端使用本地的协议来和Ceph存储集群通信。Ceph将它的功能打包到librados库中,因此你可以创建自己的Ceph客户端。下面的图表描述了这种基础架构。 basic architecture

本地协议和librados

现在的应用程序需要一个支持异步通信的简单的对象存储接口。Ceph存储集群提供了这样的一个功能。接口提供了直接的,并行的对象访问功能。

  • Pool操作
  • 快照和写时复制克隆
  • 读写对象-创建和移除-整个对象或字节范围-追加或者截短
  • 创建/设置/获取/移除 XATTRS
  • 创建/设置/获取/移除 键值对
  • 复合操作和二次ack语义
  • 对象类

对象监视和通知

客户端可以注册一个对对象的持久关注,保持一个和主OSD的会话打开。客户端可以发送通知消息和一些负载信息给所有的监视者,然后所有监视客户端都会收到通知。这使得客户端可以使用任何对象作为同步通信通道。 watch and notify

数据分片

存储设备都会有吞吐量的限制,这对性能和可扩展性都有很大的影响。大多数存储系统都支持在多个存储设备上分别存储数据的一部分片段,这会增加吞吐量和性能。最常见的就是RAID了,Ceph的分片和RAID 0类似。 Ceph提供了三类客户端: Ceph块设备、Ceph文件系统、Ceph对象存储。Ceph客户端负责转换数据给用户(一个块设备镜像,RESTful对象,CephFS文件系统目录)。

提示: Ceph存储的对象不会被分片。Ceph对象存储,Ceph块设备,Ceph文件系统将它们的数据分片存储成多个Ceph存储系统对象。Ceph客户端通过librados写入数据必须执行分片(以及并行io)才能获取分片的优点。

最简单的Ceph分片格式包含一个对象的分片。Ceph客户端向存储对象写入分片单元,直到对象达到了它的最大容量,然后创建一个新的对象来存储多出来的数据分片。这种最简单的方式对于小的块设备镜像、s3或swift对象以及CephFS文件是足够的。当然,这种简单的方式没有最大化利用Ceph分布式存储数据的优点,串行的方式也没有提升性能。下面的图标描述了这种形式的分片: simplest strip 如果你想要更大的镜像尺寸,更大的S3或Swift对象,或者更大的CephFS目录,你应该考虑通过将数据分片分布到一个对象集合中的多个对象上来提升读写性能。并行操作会显著的提升写入性能。因为对象会被映射到不同的PG中,然后被映射到不同的OSD中,每一个写入操作都会以最大的写入速度来并行操作。单硬盘的写入会因为柱头的移动和设备的最大带宽(100M/s)而受到限制(比如每次寻道都要6ms)。 通过将写入分布到多个对象上,Ceph可以减少每个硬盘的寻道时间,然后将多个硬盘的吞吐量结合以达到更快的写入(或读取)速度。

注意:分片是独立于对象复制的。因为CRUSH在OSD中复制对象,分片也会自动被复制。

在下图中,客户端数据被分片到一个对象集合中(对象集合1),这个集合包含4个对象,第一个分片单元是位于object 0 的 strip unit0,第4个分片单元是位于object3的strip unit3。当写如第4个分片后,客户端判断对象集合是否满了。如果对象集合没有满,客户端会从第一个对象继续写入分片。如果对象集合满了,客户端重新创建一个对象集合(object set2)。然后开始写入第一个分片(strip unit16)。 strip to multiple object 下面是三个重要的变量决定了Ceph如何对数据分片:

  • 对象大小: Ceph存储系统中的对象有一个配置的最大容量(比如2MB, 4MB)。这个对象大小应该足够大,以容纳很多个分片单元。

  • 分片宽度:分片有一个配置的单元大小(比如64kb)。Ceph客户端分割数据成同样大小的分片单元,除了最后一个。分片宽度应该是对象大小的一小部分,这样一个对象就可以包含多个分片单元了。

  • 分片数量:Ceph客户端在指定分片数量的对象上写入一连串的分片单元。这些指定数量的对象叫做一个对象集合。在Ceph客户端写完对象集合中的最后一个对象后,它又开始从对象集合中的第一个对象开始写入。

    重要:在投入到生产环境之前应该测试分片配置的性能。写入数据之后就不能修改这些配置了。

    一旦Ceph客户端将数据分片,然后将分片单元映射到对象中,Ceph的CRUSH算法将对象映射到PG中,然后PG被映射到Ceph OSD服务中。

    Ceph客户端

    Ceph客户端包含一系列的服务接口,包括:

    • 块设备:块设备(RBD)服务提供可变大小的、精简配置的块设备,块设备支持快照和复制。Ceph在整个集群中将块设备分片来提高性能。Ceph支持内核对象(KO)和QEMU的管理程序直接使用librbd——避免虚拟系统的内核对象过载。

    • 对象存储:对象存储(RGW)服务提供RESTful API,和Amazon S3以及OpenStack Swift兼容。

    • 文件系统:文件系统(CephFS)服务提供了POSIX兼容的文件系统,可以使用mount这样的命令,或者作为一个用户空间的文件系统。

Ceph可以运行OSD, MDS, 监控服务的额外实例,这些会提高可扩展性和高可用性。下面的图从高层架构上描述了这个情况: high level architecture

Ceph对象存储

Ceph对象存储服务叫做radosgw,是一个FastCGi程序,提供了RESTful HTTP API来存储对象和元数据。它是在Ceph存储集群的顶层,拥有自己的数据格式,维护自己的用户数据库,认证和访问控制。RADOS入口使用统一的命名空间,这意味着你既可以使用OpenStack Swift兼容的API,也可以使用Amazon S3兼容的API。举例来说,你可以使用S3的API写入数据,使用Swift的API读取数据。

Ceph块设备

Ceph块设备将一个块设备镜像分片到多个Ceph存储集群的对象上,每一个对象都会映射到不同的PG,PG也会分布到不同的ceph-osd服务上。 精简配置的,可生成快照的Ceph块设备是虚拟化和云计算的吸引点。在虚拟机的场景下,人们可以在QEMU/KVM中使用rbd网络存储驱动来部署ceph块设备,主机使用librbd来给客户提供块设备服务。需要云计算栈使用libvirt来集成管理程序。你可以在QUME中使用精简配置的Ceph块设备,使用libvirt来支持OpenStack、CloudStack或其他解决方案。 因为我们目前不提供librbd支持其他的管理程序,你也同样可以使用Ceph块设备内核对象来提供一个块设备给一个客户端。其他的虚拟化技术,比如Xen可以访问Ceph块设备的内核对象。这可以使用命令行工具rbd完成。

Ceph文件系统

Ceph文件系统(CephFS)提供了POSIX兼容的文件系统,处于基于对象的Ceph存储集群之上。 ceph filesystem Ceph文件系统服务包括Ceph元数据服务(MDS)。MDS的目的是存储所有的文件系统元数据(目录,文件拥有者,访问模式等等),元数据都是存储在内存中的。原因是MDS是的一般操作(ls, cd)会对Ceph OSD服务造成巨大的压力。因此将元数据和数据本身分离开是提供高性能服务的必要条件。

一些不错的文章

https://zhuanlan.zhihu.com/p/58888246

分类:分布式存储标签:#ceph #分布式存储

cfssl生成证书并部署

安装

cfssl是基于go的,因此需要安装go

go get -u github.com/cloudflare/cfssl/cmd/cfssl
go get -u github.com/cloudflare/cfssl/cmd/cfssljson

安装完成后如果不能直接使用cfssl请检查$GOPATH/bin是否加入到PATH环境变量中。 执行cfssl查看一下cfssl提供的命令

Usage:
Available commands:
        sign
        serve
        gencsr
        ocspsign
        ocspserve
        revoke
        certinfo
        version
        crl
        gencert
        ocsprefresh
        scan
        info
        print-defaults
        bundle
        genkey
        gencrl
        ocspdump
        selfsign
Top-level flags:

创建CA

CA的全称是Certificate Authority,也叫证书授权中心。CA的作用是作为一个权威的、被信任的第三方机构,提供管理和签发证书。在使用https访问一个网站时,为了证明这个网站是可信任的,那么就需要使用CA颁发的证书来证明自己。 因为在内网环境中搭建docker-registry,必然不可能用到互联网上的CA机构,因此我们需要自己扮演这个角色。首先创建文件夹

mkdir ca
cfssl print-defaults config > ca/ca-config.json
cfssl print-defaults csr > ca/ca-csr.json

然后修改ca-config.json

{
    "signing": {
        "default": {
            "expiry": "2540400h"
        },
        "profiles": {
            "www": {
                "expiry": "2540400h",
                "usages": [
                    "signing",
                    "key encipherment",
                    "server auth"
                ]
            },
            "client": {
                "expiry": "2540400h",
                "usages": [
                    "signing",
                    "key encipherment",
                    "client auth"
                ]
            }
        }
    }
}

生成ca

cfssl gencert -initca ca/ca-csr.json | cfssljson -bare ca/ca -

生成服务器证书

mkdir server
cfssl print-defaults csr > server/server.json

修改server.json

{
    "CN": "docker-registry",
    "hosts": [
        "docker-registry.k8s",
        "www.docker-registry.k8s"
    ],
    "key": {
        "algo": "ecdsa",
        "size": 256
    },
    "names": [
        {
            "C": "CN",
            "ST": "SH",
            "L": "Shanghai"
        }
    ]
}

签发证书

cfssl gencert -ca=ca/ca.pem -ca-key=ca/ca-key.pem -config=ca/ca-config.json -profile=www server/server.json | cfssljson -bare server/server

使用

上面两步得到了ca.pem, server-key.pem, server.pem。ca.pem是ca的证书,server-key.pem是服务器证书的私钥,server.pem是服务器证书 为了在k8s中使用,我们需要先创建默认的tls secret

kubectl -n docker-registry create secret tls docker-registry-tls-cert --key=server/server-key.pem --cert=server/server.pem

为电脑导入ca证书

sudo mkdir /usr/share/ca-certificates/extra
sudo cp ca.pem /usr/share/ca-certificates/extra/ca.crt

运行命令更新证书

sudo dpkg-reconfigure ca-certificates

然后使用curl查看ca根证书是否安装成功

curl -v https://docker-registry.k8s

显示一下内容表示成功

* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.2 (IN), TLS handshake, Certificate (11):
* TLSv1.2 (IN), TLS handshake, Server key exchange (12):
* TLSv1.2 (IN), TLS handshake, Server finished (14):
* TLSv1.2 (OUT), TLS handshake, Client key exchange (16):
* TLSv1.2 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.2 (OUT), TLS handshake, Finished (20):
* TLSv1.2 (IN), TLS handshake, Finished (20):
* SSL connection using TLSv1.2 / ECDHE-ECDSA-AES256-GCM-SHA384
* ALPN, server accepted to use http/1.1
* Server certificate:
*  subject: C=CN; ST=SH; L=SH; CN=DR
*  start date: Jun 26 06:37:00 2019 GMT
*  expire date: Apr 17 06:37:00 2309 GMT
*  subjectAltName: host "docker-registry.k8s" matched cert's "docker-registry.k8s"
*  issuer: C=US; ST=CA; L=San Francisco; CN=example.net
*  SSL certificate verify ok.
> GET / HTTP/1.1
> Host: docker-registry.k8s
> User-Agent: curl/7.64.0
> Accept: */*
> 
< HTTP/1.1 200 OK
< Server: nginx/1.15.9
< Date: Wed, 26 Jun 2019 11:40:04 GMT
< Content-Length: 0
< Connection: keep-alive
< Cache-Control: no-cache
< 
* Connection #0 to host docker-registry.k8s left intact

这里有一点需要注意的,我的电脑上安装了anaconda的环境,因此curl是在anaconda下的curl,它会默认加载/home/username/anaconda3/certs/cacert.pem这个。因此我需要将/etc/ssl/certs/ca-certificates.crt覆盖掉这个文件才能默认加载。或者也可以使用--cacert指定根目录文件的位置。 或者可以参考这篇详细的说明: https://jite.eu/2019/2/6/ca-with-cfssl/#

chrome下的相关设置

即使在我将根证书设置好并且验证成功,但是chrome仍然会有不安全的标识,这是因为需要为chrome单独导入根证书,设置的路径为: 设置 -> 高级 -> HTTPS相关设置。

分类:k8s
go处理多态的JSON

go处理多态的JSON

最近使用go在对h2o的REST API进行封装时,发现了h2o的JSON返回值中有Polymorphic类型的字段。Polymorphic可以翻译为多态。一个多态的类型可以理解为既可能是float型,也可能是string类型等等。

在我的理解中,一个JSON中每个字段的类型都必须是确定的,在动态语言比如php, js这种,处理这种不确定的类型很方便。但是在go这种静态语言中,一个不确定的类型导致在解析中总是会出现类似于这样子的错误: json: cannot unmarshal string into Go struct field Foo.Value of type int

使用interface{}处理多态的问题

在go中,interface{}是一个空接口,那么所有类型都可以看做实现了这个空接口,因此interface{}可以接收任何类型的值。那么如果一个json既可能是{"mean": 123}, 又可能是{"mean": "NaN"},就可以使用以下结构体:

type Prediction struct {
    Mean interface{} `json:"mean"`
}

但是在使用Mean这个变量的时候,就比较麻烦了。

t2 := `{"mean": "NaN"}`

var p2 Prediction
err = json.Unmarshal([]byte(t2), &p2)

if err != nil {
    log.Println(err)
} else {
    if mean, ok := p2.Mean.(string); ok {
        log.Printf("p2 mean: %s \n", mean)
    } else if n, ok := p2.Mean.(int); ok {
        mean = strconv.Itoa(n)
        log.Printf("p2 mean: %s \n", mean)
    }
}

实现Unmarshaler和Marshaler接口

type Marshaler interface {
        MarshalJSON() ([]byte, error)
}

type Unmarshaler interface {
        UnmarshalJSON([]byte) error
}

在使用json.Unmarshaljson.Marshal时,会自动调用对应变量类型的UnmarshalJSONMarshalJSON方法。这样就可以定义一个FlexString类型

type FlexString string
type Prediction struct {
    Mean FlexString `json:"mean"`
}

然后实现*FlexString的UnmarshalJSON方法和FlexString的MarshalJSON方法。

func (fs *FlexString) UnmarshalJSON(b []byte) error {
    if b[0] == '"' {
        return json.Unmarshal(b, (*string)(fs))
    }
    var n float64
    if err := json.Unmarshal(b, &n); err != nil {
        return err
    }

    s := strconv.FormatFloat(n, 'g', 15, 64)

    *fs = FlexString(s)
    return nil
}

func (fs FlexString) MarshalJSON() ([]byte, error) {
    s := string(fs)
    n, err := strconv.ParseFloat(s, 64)
    if err != nil {
        return json.Marshal(s)
    }

    if math.IsNaN(n) {
        return json.Marshal(s)
    }

    return json.Marshal(n)
}

这样在使用的时候,就可以正常的对多态的Mean进行json解码以及编码了。

func main() {
    t1 := `{"mean": 123.1212}`
    var p1 Prediction
    err := json.Unmarshal([]byte(t1), &p1)
    if err != nil {
        log.Println(err)
    } else {
        log.Println("p1 mean value:", p1.Mean)
    }

    res1, err := json.Marshal(p1)
    if err != nil {
        log.Println("res1 error:", err)
    } else {
        log.Println("res1 :", string(res1))
    }

    t2 := `{"mean": "NaN"}`
    var p2 Prediction
    err = json.Unmarshal([]byte(t2), &p2)
    if err != nil {
        log.Println(err)
    } else {
        log.Println("p2 mean value:", p2.Mean)
    }

    res2, err := json.Marshal(p2)
    if err != nil {
        log.Println("res2 error:", err)
    } else {
        log.Println("res2 :", string(res2))
    }
}

运行结果如下:

2019/07/09 16:38:42 p1 mean value: 123.1212
2019/07/09 16:38:42 res1 : {"mean":123.1212}
2019/07/09 16:38:42 p2 mean value: NaN
2019/07/09 16:38:42 res2 : {"mean":"NaN"}

123.1212在解码再编码之后,仍然是float类型,NaN仍然是string类型。这里有一个注意事项,就是n, err := strconv.ParseFloat(s, 64)这个方法,如果s是NaN的字符串并不会出错,而是将n变成一个NaN,因此需要使用math.IsNaN进行检查。

分类:go标签:#go #UnmarshalJSON #MarshalJSON #json

基于phpx的php 扩展调试

用c写php的扩展不是一件轻松的事情,调试起来更是麻烦。普通的php扩展调试网上可以找到资料。我这里因为要用到c++,因此用了phpx来降低php扩展开发的门槛。

phpx项目地址:https://github.com/swoole/phpx

准备环境

ubuntu16.04, gcc, git, wget

首先从php的官方网站下载需要的php版本的源代码: https://php.net/releases/

我下载的是php-7.0版本的

wget https://www.php.net/distributions/php-7.0.33.tar.gz

下载完成后,解压进入目录,安装一些必要的依赖库。

sudo apt-get install libxml2-dev
./configure --enable-debug
make -j 4
sudo make install

因为我的扩展使用了phpx这个库,所有phpx的编译也需要开启debug模式,编辑cmakelist.txt加入 SET(CMAKE_BUILD_TYPE "Debug")。然后

cmake .
make -j 4
sudo make install

注意:在编译php扩展的时候也要开启调试模式。

使用gdb调试

因为php+phpx这个组合编译出来的扩展没有办法像普通的gdb调试一样,通过行号来打断点调试。因此需要根据函数名来打断点。

比如我的代码中有一个关键的方法名叫做input, 因此可以

nm /usr/local/lib/php/extensions/debug-non-zts-20151012/php_dv.so | grep input

得到以下的查找结果

000000000003a99e T _Z8Dv_inputRN3php6ObjectERNS_4ArgsERNS_7VariantE
00000000000491f8 T _ZN11DataAdaptor5inputESt6vectorIP6PersonSaIS2_EE

_ZN11DataAdaptor5inputESt6vectorIP6PersonSaIS2_EE这个就是可以打断点的函数名啦。

gdb php
break _ZN11DataAdaptor5inputESt6vectorIP6PersonSaIS2_EE
run paintAll.php

就可以看到断点打在了

Breakpoint 1, DataAdaptor::input (this=0x1259de0, 
    allPerson=std::vector of length 2622, capacity 2622 = {...}) at ../DataAdaptor.cc:195
195    void DataAdaptor::input(vector allPerson) {

可以使用layout分割窗口,大致结果如下:

gdb调试窗口

coredump信息的查看

php扩展出错后,也可以生成coredump文件。需要修改limits.conf中的core文件大小,比如设置成unlimited。生成core文件后,执行gdb php core, 然后使用bt即可打印出方法的调用栈,从而分析问题出现的原因。

分类:php标签:#php #gdb #phpx #调试

如何在go中优雅的热升级服务

一、概述

在日常业务中,服务会经常升级,但是因为某些原因不希望断开和客户端的连接。因此就需要服务的热升级技术。 在研究这个问题之前,可以先看一下nginx是如何做到不间断服务热重启的。

  • 将新的nginx可执行文件替换掉旧的可执行文件。

  • master进程发送USR2信号,master进程在接收到信号后会将pid文件命名为.oldbin后缀。之后启动新的可执行文件,并启动新的worker进程。这个时候会有两个master进程

  • 向第一个master进程发送WINCH信号。第一个master进程会通知旧的worker进程优雅(处理完当前请求)地退出。

  • 如果此时新的可执行文件有问题。可以做以下措施:

  • 向旧的master进程发送HUP信号,旧的master进程会启动新的worker进程,并且不会重新读取配置文件。然后会向新的master进程发送QUIT信号来要求其退出。

  • 发送TERM信号到新的master进程,新的master进程和其派生的worker进程都会立刻退出。旧的master进程会自动启动新的worker进程。

  • 如果升级成功,QUIT信号会发送给旧的master进程,并退出。

完整的文档可以看: http://nginx.org/en/docs/control.html#upgrade 在go中处理这个问题也是这个思路。

二、具体实现

2.1 定义服务

type Server struct {
l         net.Listener  // 监听端口
conns     map[int]net.Conn  // 当前服务的所有连接
rw        sync.RWMutex      // 读写锁,用来保证conns在并发情况下的正常工作
idLock    sync.Mutex        // 锁,用来保证idCursor在并发情况下的递增没有问题
idCursor  int               // 用来标记当前连接的id
isChild   bool // 是否是子进程
status    int  // 当前服务的状态
relaxTime int  // 在退出时允许协程处理请求的时间
}

2.2 处理信号

func (s *Server) handleSignal() {
sc := make(chan os.Signal)

signal.Notify(sc, syscall.SIGHUP, syscall.SIGTERM)

for {
sig := <-sc

switch sig {
case syscall.SIGHUP:
log.Println("signal sighup")
// reload
go func() {
s.fork()
}()
case syscall.SIGTERM:
log.Println("signal sigterm")
// stop
s.shutdown()
}
}
}

这里只处理了两个信号,HUP表示要热升级服务,此时会fork一个新的服务。TERM表示要终止服务。

2.3 如何fork新的服务

func (s *Server) fork() (err error) {

log.Println("start forking")
serverLock.Lock()
defer serverLock.Unlock()

if isForked {
return errors.New("Another process already forked. Ignoring this one")
}

isForked = true

files := make([]*os.File, 1+len(s.conns))
files[0], err = s.l.(*net.TCPListener).File() // 将监听带入到子进程中
if err != nil {
log.Println(err)
return
}

i := 1
for _, conn := range s.conns {
files[i], err = conn.(*net.TCPConn).File()

if err != nil {
log.Println(err)
return
}

i++
}

env := append(os.Environ(), CHILD_PROCESS+"=1")
env = append(env, fmt.Sprintf("%s=%s", SERVER_CONN, strconv.Itoa(len(s.conns))))

path := os.Args[0] // 当前可执行程序的路径
var args []string
if len(os.Args) > 1 {
args = os.Args[1:]
}

cmd := exec.Command(path, args...)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.ExtraFiles = files
cmd.Env = env

err = cmd.Start()
if err != nil {
log.Println(err)
return
}

return
}

这里会将监听的文件描述符以及所有连接的文件描述符都带到新的服务中。这里只需要在新的服务中重新使用这些文件描述符即可保证不断开连接。

2.4 服务的启动流程

func (s *Server) Start(addr string) {
var err error

log.Printf("pid: %v \n", os.Getpid())

s.setState(StateInit)

if s.isChild {
log.Println("进入子进程")
// 通知父进程停止
ppid := os.Getppid()

err := syscall.Kill(ppid, syscall.SIGTERM)

if err != nil {
log.Fatal(err)
}

// 子进程, 重新监听之前的连接
connN, err := strconv.Atoi(os.Getenv(SERVER_CONN))
if err != nil {
log.Fatal(err)
}

for i := 0; i < connN; i++ {
f := os.NewFile(uintptr(4+i), "")
c, err := net.FileConn(f)
if err != nil {
log.Print(err)
} else {
id := s.add(c)
go s.handleConn(c, id)
}
}
}

s.l, err = s.getListener(addr)
if err != nil {
log.Fatal(err)
}
defer s.l.Close()

log.Println("listen on ", addr)

go s.handleSignal()

s.setState(StateRunning)

for {
log.Println("start accept")
conn, err := s.l.Accept()
if err != nil {
log.Fatal(err)
return
}

log.Println("accept new conn")

id := s.add(conn)
go s.handleConn(conn, id)
}

}

func (s *Server) getListener(addr string) (l net.Listener, err error) {
if s.isChild {
f := os.NewFile(3, "")
l, err = net.FileListener(f)
return
}

l, err = net.Listen("tcp", addr)

return
}

启动时,会判断是否是fork出的新的进程。如果是,则继承从父进程传递过来的文件描述符,并重新监听或作为连接处理。 完整的代码参考github: https://github.com/joyme123/graceful\_restart\_server\_in\_golang\_demo

分类:go标签:#go #hot upgrade

go WebAssembly初体验

WebAssembly是一门新的浏览器技术。可以取代一部分js的角色,并且从性能上来说,要比js好很多。WebAssembly目前还处于早期的发展阶段,仍然不够成熟。go语言对WebAssembly的支持也是在1.11版本上刚刚加入。虽然不能投入生产环境,但是可以用来做一些很有意思的事情。 官方WebAssembly的文档:https://github.com/golang/go/wiki/WebAssembly。详细的介绍可以看官方的文档。

一个go WebAssembly的例子

main.go

package main

import "fmt"

func main() {
    fmt.Println("hello WebAssembly")
}

编译这个go文件: GOOS=js GOARCH=wasm go build -o main.wasm main.go index.html

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>Go wasm</title>
</head>
<body>
    <script src="wasm_exec.js"></script>
    <script>
        const go = new Go();
        WebAssembly.instantiateStreaming(
            fetch("main.wasm"),go.importObject).then((result) => {
            go.run(result.instance)
        });

    </script>
</body>
</html>

这里还需要一个wasm_exec.js。这个文件是官方提供的,可以理解为是go编译出来的二进制文件和js间的连接桥梁。这个时候打开浏览器,可以看到控制台下有:hello WebAssembly。 需要说明的是,在go里面的所有STDOUT,都会在浏览器的控制台打印出来。

wasm的初始化过程

WebAssembly.instantiateStreaming(fetch("main.wasm"),go.importObject)。浏览器执行这样一段代码来加载main.wasm,并返回一个Promise对象。在加载完毕之后,调用go.run(result.instance)来执行wasm中的代码。 加载一个wasm文件就是这样的简单。

go WebAssembly如何和js交互

上面的例子非常简单。但是WebAssembly技术绝非这样简单。我们可以在go中轻松的调用js中的方法。比如下面这句话:

js.Global().Get("document").Call("getElementById", "maxCubes").Set("value", 256)

相当于js中的

document.getElementById("maxCubes).setAttribute('value', 256)

同样的,我们也可以在go中定义js方法,这样就可以在js中直接调用了。

api.onMemInitCb = js.NewCallback(func(args []js.Value) {
        length := args[0].Int()
        api.console.Call("log", "length", length) // 调用js的console.log("length", length)
        api.inBuf = make([]uint8, length)
        // 拿到这个slice的SliceHeader
        hdr := (*reflect.SliceHeader)(unsafe.Pointer(&api.inBuf))
        ptr := uintptr(unsafe.Pointer(hdr.Data))

        api.console.Call("log", "ptr:", ptr)
        js.Global().Call("gotMem", ptr)

        fmt.Println("初始化Mem成功")
    })

js.Global().Set("initMem", api.onMemInitCb)

这样,我们就可以在js中直接使用initMem方法了。这样子,就相当于打通了go和js之间的通道,使得go WebAssembly几乎无所不能。

go和js之间如何通过内存传值。

这个部分是我在看go WebAssembly部分时最关注的。因为大多数时候,我们传参都不仅仅是256这样的字面值。比如我们在做图片处理的时候,浏览器加载图片后传给wasm去处理,这种时候传递的肯定是一个指向一段内存的指针。 go代码中:

api.onMemInitCb = js.NewCallback(func(args []js.Value) {
    length := args[0].Int()
    api.console.Call("log", "length", length) // 调用js的console.log("length", length)
    api.inBuf = make([]uint8, length)
    // 拿到这个slice的SliceHeader
    hdr := (*reflect.SliceHeader)(unsafe.Pointer(&api.inBuf))
    ptr := uintptr(unsafe.Pointer(hdr.Data))

    api.console.Call("log", "ptr:", ptr)
    js.Global().Call("gotMem", ptr)

    fmt.Println("初始化Mem成功")
})

js代码中:

function gotMem(pointer) {

    console.log("pointer", pointer)

    memoryBytes.set(bytes, pointer);
    // Now the image can be loaded from the slice.

    console.log("load image")
    loadImage();
}

......

let reader = new FileReader();
reader.onload = (ev) => {
    bytes = new Uint8Array(ev.target.result);
    initMem(bytes.length);
    let blob = new Blob([bytes], {'type': imageType});
    document.getElementById("sourceImg").src = URL.createObjectURL(blob);
};
imageType = this.files[0].type;
reader.readAsArrayBuffer(this.files[0]);

上面有两段代码,js部分代码是在加载一张图片,然后转换为Uint8Array数组bytes,然后调用initMem方法,传递一个数组bytes的长度作为参数。而initMem是在go代码中定义的,initMem负责调用make([]uint8, length)去初始化需要的内存。然后通过一系列的转换获得申请的内存区域的指针ptr。然后调用js的gotMem将这个ptr传递给js代码。在gotMem中,memoryBytes.set(bytes, pointer)这句代码初始化了这块内存。 这里值得指出的是,memoryBytes是一段在wasm初始化时申请的内存,保存在一个Uint8Array数组中。如果我们打印出来的话,可以发现这段内存有1G。然后我们的go代码中调用make([]uint8, length)申请一块内存时,其实是在这段内存中申请的。比如说申请的区域为201883648~(201883648+107003)。我们只要在js中向memoryBytes的数组中给这块区域赋值,就把值传递给go的对象了。 再考虑一个问题。1G的初始化内存是不是太大了?这个内存是在编译go代码时由编译工具指定的。但是如果我们使用浏览器(比如chrome)的任务管理器查看这个窗口的占用内存时就会发现,实际占用并不会这么大。 在我的理解中(并不一定正确),这和C语言的malloc类似。malloc可以申请大于物理内存的虚拟内存,但是只要你不实际占用这么大内存,是不会有问题的。所以虽然go WebAssembly打印出来有1G的初始化内存,但是如果不是真的会使用,是不会占用这么大物理内存的。 关于初始化内存过大的问题,可以参考这个issues:cmd/compile: wasm code causes out of memory error on Chrome and Firefox for Android 一些关于WebAssembly内存的设计:Finer-grained control over memory

一个用go WebAssembly实现中位切分法处理图片的例子

关于中切分法可以参考我之前的这篇文章:中位切分法颜色量化 可以在这个地址进行预览: 预览地址 在浏览器中运行的效果(这里只展示了FastMap的效果,BestMap会好很多): fastmap 代码的github地址是:WebAssembly-MedianCut 里面大多数的代码都是用的我之前的代码。可见go WebAssembly还可以非常舒服的复用之前的代码。

分类:go标签:#go #WebAssembly

中位切分法颜色量化

首先举两个例子。 有一种视频接口叫VGA,这种视频接口有一个最大的缺点,就是同一时刻无法显示超过256种颜色。而对于一张true-color的图片,R、G、B都是有1个byte标示,因此一张true-color的图片可能有2^24种颜色。这远远超过了VGA可以同时展示的颜色数量。 我们都知道有一种图片格式叫PNG( PNG格式分析与压缩原理)。它的编码方案中,有一种使用调色板的编码方案。调色板上最多有256种颜色。这样每一种颜色都可以用一个byte的索引值代替。如果将一副超过256种颜色的png图使用调色板的编码方式,那么就可以明显的减少图片的体积。 那么如何将2^24种颜色用256种颜色表示呢?或者说,如何将m种颜色使用n种颜色来代替表示(m>n)。这就是这篇文章主要讨论的问题。 首先,如下图所示,RGB可以映射到三维空间中,R代表X轴,G代表Y轴,B代表Z轴。这样,任意一个RGB的组合都可以在空间中以一个点表示。 rgb-cube 对于一个24-bit的图片来说,通常来说颜色空间是连续的,因为颜色之间的最小差异是几乎不可能察觉的。现在,这个连续的颜色空间要被映射到256种离散的颜色上。将一个连续的变量映射到离散的集合上,就叫做量化。 有了上面这些说明后,现在有一张颜色很多的图片。图片上所有的点都被映射到一个空间中的立方体上。因为图片上点的分布一般不是均匀的,那么就可能有很多个点的聚集块。一般来说,聚集的越密,代表这些点的颜色越接近,那么如果我们将这些聚集块的点用聚集块中心点的颜色来表示,这样就可以将很多接近的颜色用一种颜色来代替。其实,这个过程跟聚类是很相似的。只不过一般的聚类算法都是无监督的机器学习,效率要较差。而中位切分法则可以用很快的速度完成这样的聚类。

中位切分法

中位切分法用简短的话描述:将一个图片对应的RGB立方体切割成目标数量的紧凑的RGB立方体,然后用立方体的质心值代表立方体内所有点的值。重复这个过程直到得到想要的颜色数量。 这里假设我们要获取256个颜色。详细的流程如下:

  • 将整张图片转换成一个RGB立方体
  • 找到立方体的最长边,从中位数的地方开始切割。得到两个包含相同数量点的立方体。
  • 对分割成的立方体重复上一步的切割过程直到得到256个立方体。
  • 256个立方体的质心就是要计算的256个颜色值。

用一个例子来说明(为了简单,这里没有B颜色空间),这里有6种颜色,共14个点。想要得到4中颜色输出。

  1. 初始情况
Color (r,g)-coordinates Count
C0 (20,40) 3
C1 (40,20) 2
C2 (5,60) 4
C3 (50,80) 2
C4 (60,30) 1
C5 (80,50) 2
  1. 3次切割后的情况
Cube HistPtr.lower HistPtr.upper Colors Enclosed Cube Centroid
A3 0 0 C0 (20,40)
B3 1 1 C2 (5,60)
A2 2 3 C1,C4 (46.7,23.3)
B2 4 5 C5,C3 (65,65)

具体流程图如下: 第一次:最长边是R,中位数是(20 + 40) / 2 = 30。从30处切割。得到收缩后的矩形。左边的矩形为A1,右边为B1。 第二次: 切割B1的R边,得到A2,B2 第三次: 切割A1的R边,得到A3,B3 这个时候我们已经有4个矩形了。 这里我们有两个颜色映射方式。第一种是快速映射,以矩形的质心作为映射后的颜色值。第二种是最佳映射,得到和其他点的距离和最短的点作为映射后的颜色值。 切割过程

中位切分法的实现

  1. 递归方式。在描述的时候,我们RGB块描述成递归的方式。如下
Split(Cube){
  if (ncubes == 4) return;
  find longest axis of Cube;
  cut Cube at median to form CubeA, CubeB;
  Split(CubeA);
  Split(CubeB);
}

但是这种递归切割的方式是有问题的。结果如下图 递归切割 在递归情况下,B1将一直不会被处理。 2.为了解决1中的问题。我们可以设定一个最大深度(level)。比如我们要得到4个输出颜色。那么log 2 4=2。最大深度应该是2。当切割到最大深度时,我们就不再往下切割,转而切割那些还未被处理的更大的区域。

maxlevel = 2;
Split(Cube,level){
  if (ncubes == 4) return;
  if (Cube's level == maxlevel) return;
  find longest axis of Cube;
  cut Cube at median to form CubeA, CubeB;
  Split(CubeA, level+1);
  Split(CubeB, level+1);
}
  1. 但是这样又会有新的问题出现。如果一个立方体中只有一种颜色(实际上此时只有一个点),不能再往下切割。因此我们需要一种方案去解决这个问题。这里可以维持一个包含所有立方体的数组,然后根据优先级排序切割。优先级可以按照level从小到大排序,但是所有颜色数量为1的立方体都会被忽略。这样每次切割前对这个数组做一次排序,取出优先级最高的立方体进行切割。直到切割出指定数量的立方体或者所有的立方体颜色都为1。
build initial cube from histogram;
set initial cube's level to 0;
insert initial cube in list of cubes;
ncubes = 1;
while (ncubes < maxcubes){
  search for Cube with smallest level;
  find the longest axis of Cube;
  find the median along this axis;
  cut Cube at median to form CubeA, CubeB;
  set CubeA's level = Cube's level + 1;
  set CubeB's level = Cube's level + 1;
  insert CubeA in Cube's slot;
  add CubeB to end of list of cubes;
  ncubes = ncubes + 1;
}

实践

  1. 使用中位切分法提取图片的主题色

  2. 使用中位切分法压缩图片

中位切分法的实现的go语言版本:joyme123/MedianCut点击这里进行效果预览

参考文献

Median-Cut Color Quantization 前端图片主题色提取

分类:图像相关, go标签:#中位切分 #颜色量化

virtualbox 网络桥接

virtualbox的默认方式是NAT,用宿主机对虚拟机做端口转发。在组建本地的集群环境时,使用这种方式是不行的。可以使用桥接网卡的方式,使得虚拟机分配到一个宿主机局域网内的ip地址。 首先,修改/etc/network/interfaces文件。 这个文件原来的内容如下:

# This file describes the network interfaces available on your system
# and how to activate them. For more information, see interfaces(5).

source /etc/network/interfaces.d/*

# The loopback network interface
auto lo
iface lo inet loopback

# The primary network interface
auto enp0s3
iface enp0s3 inet dhcp

修改成如下:

# This file describes the network interfaces available on your system
# and how to activate them. For more information, see interfaces(5).

source /etc/network/interfaces.d/*

# The loopback network interface
auto lo
iface lo inet loopback

# The primary network interface
auto enp0s3
#iface enp0s3 inet dhcp
iface enp0s3 inet static
address 192.168.0.100
gateway 192.168.0.1
netmask 255.255.255.0

注意这里的addressgateway的网段要和宿主机网段一致。比如我的宿主机ip为192.168.0.8。 注意这里还要修改一下dns,不然会出现无法解析域名的问题,编辑/etc/resolvconf/resolv.conf.d/base 添加一下的nameserver:

nameserver 8.8.8.8
nameserver 1.1.1.1

执行sudo resolvconf -u使dns的配置生效。 然后修改虚拟机的设置,在VirtualBox的菜单栏中:设备->网络->网络->连接方式:桥接网卡。 然后重启网络 sudo /etc/init.d/networking restart 如果网络还是有问题,可以尝试重启虚拟机。 之后在终端之中输入ifconfig,网络信息如下:

enp0s3    Link encap:Ethernet  HWaddr 08:00:27:f1:6d:fd  
          inet addr:192.168.0.100  Bcast:192.168.0.255  Mask:255.255.255.0
          inet6 addr: fe80::a00:27ff:fef1:6dfd/64 Scope:Link
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:304 errors:0 dropped:0 overruns:0 frame:0
          TX packets:173 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000 
          RX bytes:28254 (28.2 KB)  TX bytes:28063 (28.0 KB)

虚拟机的ip已经变成192.168.0.100。使用ping 192.168.0.8检查和宿主机的通信是否正常。

分类:架构设计, 工作, 计算机标签:#virtualbox

TLS1.2 RFC5426中一些术语解释

最近想为我的cats服务器加上https的支持。因为最近有一点点的忙,这个项目已经很久没有提交新的代码了。之所以没有用一些开源的库去做,因为这个项目的目的就是锻炼我的代码能力,以及英文RFC的阅读能力。但是在看RFC5426时遇到了一些挫折,里面有大量的密码学上的专业词汇。因此买了一本《图解密码技术》,这里将RFC5426中的专业词汇和概念单独拿出来做一次笔记。 PRF( pseudorandom function ) algorithm: 伪随机函数算法。随机数有三类性质:1.随机性。2.不可预测性。3.不可重现性。这三个性质要求越来越严格。满足1,称为弱伪随机数,满足1、2,称为强伪随机数,满足1、2、3,称为真伪随机数。在密码学的体系中,要求至少达到强伪随机数才能保证安全。 public key encryption:公开密钥加密(英语:Public-key cryptography),也称为非对称加密(英语:asymmetric cryptography),是密码学的一种算法,它需要两个密钥,一个是公开密钥,另一个是私有密钥;一个用作加密的时候,另一个则用作解密。使用其中一个密钥把明文加密后所得的密文,只能用相对应的另一个密钥才能解密得到原本的明文;甚至连最初用来加密的密钥也不能用作解密。由于加密和解密需要两个不同的密钥,故被称为非对称加密;不同于加密和解密都使用同一个密钥的对称加密。虽然两个密钥在数学上相关,但如果知道了其中一个,并不能凭此计算出另外一个;因此其中一个可以公开,称为公钥,任意向外发布;不公开的密钥为私钥,必须由用户自行严格秘密保管,绝不透过任何途径向任何人提供,也不会透露给要通信的另一方,即使他被信任。参考链接: 公开密钥加密 RSA: 名称不是什么缩写,而是发明人首字母的结合。是一种非对称加密的方法。它的速度比起DES等对称加密算法要慢的多。因为非对称加密中,有一个公钥和一个私钥,那么如果分配公钥则是一个问题。如果通过网络传输公钥,则可能被中间人替换掉公钥进行攻击.因此一般的做法是用可靠的第三方机构签发证书来防止这样的攻击。参考链接:RSA加密演算法 MAC (Message Authentication Code) algorithm: 消息验证码的算法.是经过特定算法后产生的一小段信息,检查某段消息的完整性,以及作身份验证 。它可以用来检查在消息传递过程中,其内容是否被更改过,不管更改的原因是来自意外或是蓄意攻击。同时可以作为消息来源的身份验证,确认消息的来源。在我的理解中,MAC是一种与密钥相关联的单向散列函数。参考链接:消息认证码 HMAC (Keyed-Hashing for Message Authentication): 它通过一个标准算法,在计算哈希的过程中,把key混入计算过程中。其实就是常用的加salt的方式,使得相同的原值生成不同的哈希值。HMAC是用来生成MAC值的。参考链接:密钥散列消息认证码 DSA (digital signing algorithm):DSA是一种更高级的验证方式。一般用于数字签名和认证。DSA 不单单只有公钥、私钥,还有数字签名。私钥加密生成数字签名,公钥验证数据及签名。在DSA数字签名和认证中,发送者使用自己的私钥对文件或消息进行签名,接受者收到消息后使用发送者的公钥来验证签名的真实性。如果数据和签名不匹配则认为验证失败!数字签名,不仅能验证数据的完整性,真实性,还能“对第三方证明”和“防止否认”。参考链接:常见的加密算法之DSA 算法 CBC (Cipher Block Chaining):分组密码的一种工作模式,允许使用同一个分组密码密钥对多于一块的数据进行加密,并保证其安全性。其他的工作模式还有:ECB,PCBC,CFB,OFB,CTR等。参考链接:分组密码工作模式 SHA256, SHA1, MD5: 常见的几种信息摘要算法(有时候也称为哈希算法,单向散列函数等)。 SSL (Secure Socket Layer):安全套接字层 TLS (Transport Layer Security Protocol):安全传输层协议,TLS的后续工作是在SSL的基础上进行的。 stream cipher encryption:在密码学中,流密码(英语:Stream cipher),又译为流加密、数据流加密,是一种对称加密算法,加密和解密双方使用相同伪随机加密数据流(pseudo-random stream)作为密钥,明文数据每次与密钥数据流顺次对应加密,得到密文数据流。实践中数据通常是一个位(bit)并用异或(xor)操作加密。参考链接: 流密码 block cipher encryption:在密码学中,分组加密(英语:Block cipher),又称分块加密或块密码,是一种对称密钥算法。它将明文分成多个等长的模块(block),使用确定的算法和对称密钥对每组分别加密解密。分组加密是极其重要的加密协议组成,其中典型的如DES和AES作为美国政府核定的标准加密算法,应用领域从电子邮件加密到银行交易转帐,非常广泛。参考链接: 分组加密 authenticated encryption with additional data (AEAD) encryption:认证加密(英语:Authenticated encryption,AE)和用于关联数据的认证加密(authenticated encryption with associated data,AEAD,AE的变种)是一种能够同时保证数据的保密性、 完整性和真实性的一种加密模式。参考链接: 用于关联数据的认证加密 compression algorithm:数据压缩算法 master secret:主密码用来生成对称密码的秘钥,消息认证码的秘钥以及对称密码CBC模式所使用的初始化向量(IV)

分类:网络协议标签:#TLS1.2 #RFC5426