微服务之服务监控和治理、容错隔离与 Docker 部署

.微服务监控系统

1.1 什么是监控系统

  • 一旦请求服务出现异常,我们必须得知道是在哪个服务环节出了故障,就需要对每一个服务,以及各个指标都进行全面的监控;

  • 监控系统能为我们提供具体的指标数据进行追踪和跟进。

在微服务架构中,监控系统按照原理和作用大致可以分为三类:

  • 日志监控(Log)
  • 调用链调用监控(Tracing)
  • 度量监控(Metrics)

1.2 日志监控

  • 日志类比较常见,我们的框架代码、系统环境、以及业务逻辑中一般都会产出一些日志,这些日志我们通常把它记录后统一收集起来,方便在跟踪和丁文问题的需要时进行查询;
  • 日志类记录的信息一般是一些事件、非结构化的一些文本内容;
  • 日志的输出和处理的解决方案常用的有 ELK Stack 方案,用于实时搜索,分析和可视化日志数据;
  • 开源实时日志分析 ELK 平台能够完美的解决我们上述的问题,ELK 由ElasticSearch、Logstash和Kiabana 三个开源工具组成。

图片

组件介绍

  • Elasticsearch 是个开源分布式搜索引擎,它具备分布式、零配置、自动发现、索引自动分片、索引副本机制、RESTful 风格接口、多数据源、自动搜索负载等特性;
  • Logstash 是一个完全开源的工具,它可以对你的日志进行收集、过滤,并将其存储供以后使用(如搜索);
  • Kibana 也是一个开源和免费的工具,可以为 Logstash 和 ElasticSearch 提供的日志分析友好的 Web 界面,可以帮助您汇总、分析和搜索重要数据日志;
  • Kafka 用来接收用户日志的消息队列。

工作流程图

图片

  • Logstash 收集 AppServer 产生的日志记录 Log;
  • 将日志 log 存放到 ElasticSearch 集群中,而 Kibana 则从 ES 集群中查询数据生成图表;
  • 生成的日志图表返回给 Browser 进行渲染显示,分别支持各种终端进行显示。

1.3 调用链监控

1.3.1 什么是调用链监控

  • 调用链监控是用来追踪微服务之前依赖的路径和问题定位;
  • 主要原理就是子节点会记录父节点的 id 信息。例如阿里的鹰眼系统就是一个调用链监控系统;
  • 一个请求从开始进入,在微服务中调用不同的服务节点后,再返回给客户端,在这个过程中通过调用链参数来追寻全链路的调用行程。通过这个方式可以很方便的知道请求在哪个环节出了故障,系统的瓶颈出现在哪一个环节,定位出优化点。

1.3.2 为什么需要调用链监控

  • 「调用链监控」是在微服务架构中非常重要的一环。它除了能帮助我们定位问题以外,还能帮助项目成员清晰的去了解项目部署结构。
  • 微服务如果达到了成百个服务调用,时间久了之后,项目的结构很可能就会出现超级混乱的调用,见下图:

图片

  • 在这种情况下,团队开发者甚至是架构师都不一定能对项目的网络结构有很清晰的了解,那就更别谈系统优化了。

1.3.3 调用链监控的作用

生成项目网络拓扑图

  • 根据「调用链监控」中记录的链路信息,给项目生成一张网络调用的拓扑图;
  • 通过这张图,我们就可以知道系统中的各个服务之间的调用关系是怎样的,以及系统依赖了哪些服务;
  • 可以让架构师监控全局服务状态,便于架构师掌握系统的调用结构。

快速定位问题

  • 微服务架构下,问题定位就变得非常复杂了,一个请求可能会涉及到多个服务节点;
  • 有了调用链监控系统就能让开发人员快速的定位到问题和相应模块,提升解决问题效率。

优化系统

  • 通过记录了请求在调用链上每一个环节的信息,可以通过得出的服务信息找出系统的瓶颈,做出针对性的优化;
  • 还可以分析这个调用路径是否合理,是否调用了不必要的服务节点,是否有更近、响应更快的服务节点;
  • 通过对调用链路的分析,我们就可以找出最优质的调用路径,从而提高系统的性能。

1.3.4 调用链监控的原理

主要原理就是子节点会记录父节点的 id 信息,要理解好三个核心的概念 Trace、Span 和 Annotation。

Trace

  • Trace 是指一次请求调用的链路过程,trace id 是指这次请求调用的 ID;
  • 在一次请求中,会在网络的最开始生成一个全局唯一的用于标识此次请求的 trace id。这个 trace id 在这次请求调用过程中无论经过多少个节点都会保持不变,并且在随着每一层的调用不停的传递;
  • 最终,可以通过 trace id 将这一次用户请求在系统中的路径全部串起来。

Span

  • Span 是指一个模块的调用过程,一般用 span id 来标识。在一次请求的过程中会调用不同的节点、模块、服务,每一次调用都会生成一个新的 span id 来记录;
  • 这样就可以通过 span id 来定位当前请求在整个系统调用链中所处的位置,以及它的上下游节点分别是什么。

Annotation

指附属信息,可以用于附属在每一个 Span 上自定义的数据。

具体流程:

图片

  • 从图中可见,一次请求只有一个唯一的 trace id=12345,在请求过程中的任何环节都不会改变;
  • 在这个请求的调用链中,Span A 调用了 Span B,然后 Span B 又调用了 Span C 和 Span D,每一次 Span 调用都会生成一个自己的 span id,并且还会记录自己的上级 span id 是谁;
  • 通过这些 id,整个链路基本上就都能标识出来,记录了调用过程。

1.3.5 调用链监控开源应用

CAT

  • CAT 是由大众点评开源的一款调用链监控系统,基于JAVA开发,有很多互联网企业在使用,热度非常高;
  • 它有一个非常强大和丰富的可视化报表界面,这一点其实对于一款调用链监控系统而来非常的重要;
  • 在 CAT 提供的报表界面中有非常多的功能,几乎能看到你想要的任何维度的报表数据;
  • CAT 有个很大的优势就是处理的实时性,CAT 里大部分系统是分钟级统计。

Open Zipkin

  • Zipkin 由 Twitter 开源,支持的语言非常多。它基于 Google Dapper 的论文设计而来,国内外很多公司都在用,文档资料也很丰富。
  • 用于收集服务的定时数据,以解决微服务架构中的延迟问题,包括数据的收集、存储、查找和展现。

Pinpoint

  • Pinpoint 中的服务关系依赖图做得非常棒,超出市面上任何一款产品;
  • Pinpoint 运用 JavaAgent 字节码增强技术,只需要加启动参数即可。因为采用字节码增强方式去埋点,所以在埋点的时候是不需要修改业务代码的,是非侵入式的。非常适合项目已经完成之后再增加调用链监控的时候去使用的方案;
  • 但是也是由于采用字节码增强的方式,所以它目前仅支持 Java 语言。

方案选型比较

图片

1.4 度量监控

1.4.1 什么是度量监控

  • 度量类监控主要采用时序数据库的解决方案;
  • 它是以事件发生时间以及当前数值的角度来记录的监控信息,是可以聚合运算的,用于查看一些指标数据和指标趋势;
  • 所以这类监控主要不是用来查问题的,主要是用来看趋势的,基于时间序列数据库的监控系统是非常适合做监控告警使用的。

Metrics 一般有 5 种基本的度量类型:

  • Gauges(度量)
  • Counters(计数器)
  • Histograms(直方图)
  • Meters(TPS计算器)
  • Timers(计时器)

1.4.2 时序数据库有哪些

Prometheus

  • Promethes 是一款 2012 年开源的监控框架,其本质是时间序列数据库,由 Google 前员工所开发;
  • Promethes 采用拉的模式(Pull)从应用中拉取数据,并还支持 Alert 模块可以实现监控预警。它的性能非常强劲,单机可以消费百万级时间序列。

图片

从图的左下角可以看到,Prometheus 可以通过在应用里进行埋点后 Pull 到 Prometheus Server 里。如果应用不支持埋点,也可以采用 exporter 方式进行数据采集。

从图的左上角可以看到,对于一些定时任务模块,因为是周期性运行的,所以采用拉的方式无法获取数据,那么 Prometheus 也提供了一种推数据的方式,但是并不是推送到 Prometheus Server 中,而是中间搭建一个 Pushgateway。定时任务模块将 metrics 信息推送到这个 Pushgateway 中,然后 Prometheus Server 再依然采用拉的方式从 Pushgateway 中获取数据。

需要拉取的数据既可以采用静态方式配置在 Prometheus Server 中,也可以采用服务发现的方式(即图的中间上面的 Service discovery 所示)。

PromQL 是 Prometheus 自带的查询语法,通过编写 PromQL 语句可以查询 Prometheus 里面的数据。Alertmanager 是用于数据的预警模块,支持通过多种方式去发送预警。WebU 用来展示数据和图形,但是一般大多数是与 Grafana 结合,采用 Grafana 来展示。

OpenTSDB

  • OpenTSDB 是在 2010 年开源的一款分布式时序数据库,当然其主要用于监控方案中;
  • OpenTSDB 采用的是 Hbase 的分布式存储,它获取数据的模式与 Prometheus 不同,它采用的是推模式(Push);
  • 在展示层,OpenTSDB 自带有 WebUI 视图,也可以与 Grafana 很好的集成,提供丰富的展示界面;
  • 但 OpenTSDB 并没有自带预警模块,需要自己去开发或者与第三方组件结合使用。

图片

InfluxDB

  • InfluxDB 是在 2013 年开源的一款时序数据库,在这里我们主要还是用于做监控系统方案;
  • 它收集数据也是采用推模式(Push)。在展示层,InfluxDB 也是自带 WebUI,也可以与 Grafana 集成。

图片

1.5 微服务监控体系

监控是微服务治理的重要环节,架构采用分层监控,一般分为以下监控层次。如下图所示:

图片

系统层

系统层主要是指 CPU、磁盘、内存、网络等服务器层面的监控,这些一般也是运维同学比较关注的对象。

应用层

应用层指的是服务角度的监控,比如接口、框架、某个服务的健康状态等,一般是服务开发或框架开发人员关注的对象。

用户层

这一层主要是与用户、与业务相关的一些监控,属于功能层面的,大多数是项目经理或产品经理会比较关注的对象。

监控指标

  • 延迟时间:主要是响应一个请求所消耗的延迟,比如某接口的 HTTP 请求平均响应时间为 100ms;
  • 请求量:是指系统的容量吞吐能力,例如每秒处理多少次请求(QPS)作为指标。
  • 错误率:主要是用来监控错误发生的比例,比如将某接口一段时间内调用时失败的比例作为指标。

2. 微服务容错隔离

2.1 什么是容错隔离

单体应用的架构下一旦程序发生了故障,那么整个应用可能就没法使用了,所以我们要把单体应用拆分成具有多个服务的微服务架构,来减少故障的影响范围。

但是在微服务架构下,有一个新的问题就是,由于服务数变多了,假设单个服务的故障率是不变的,那么整体微服务系统的故障率其实是提高了的。

假设单个服务的故障率是 0.01%,也就是可用性是 99.99%,如果我们总共有 10 个微服务,那么我们整体的可用性就是 99.99% 的十次方,得到的就是 99.90% 的可用性(也就是故障率为 0.1%)。可见,相对于之前的单体应用,整个系统可能发生故障的风险大幅提升。

当某个服务出现故障,我们要做的就是最大限度的隔离单个服务的风险,也就是「 容错隔离 」的方法。不仅保证了微服务架构的正常运行,也保证了系统的可用性和健壮性。

2.2 常见的可用性风险有哪些

单机可用性风险

  • 单机可用性风险指的是微服务部署所在的某一台机器出现了故障,造成的可用性风险;
  • 这种风险发生率很高,因为单机器在运维中本身就容易发生各种故障,例如硬盘坏了、机器电源故障等等,这些都是时有发生的事情;
  • 不过虽然这种风险发生率高,但危害有限,因为我们大多数服务并不只部署在一台机器上,可能多台都有,因此只需要做好监控,发现故障之后,及时的将这台故障机器从服务集群中剔除即可,等修复后再重新上线到集群里。

单机房可用性风险

  • 这种风险的概率比单机器的要低很多,但是也不是完全不可能发生,在实际情况中,还是有一定概率的。比如最为常见的就是通往机房的光纤被挖断,造成机房提供不了服务;
  • 如果我们的服务全部都部署在单个机房,而机房又出故障了,但是现在大多数中大型项目都会采用多机房部署的方案,比如同城双活、异地多活等;
  • 一旦某个机房出现了故障不可用了,立即采用切换路由的方式,把这个机房的流量切到其它机房里就能正常提供服务了。

跨机房集群可用性风险

  • 跨机房集群只是保证了物理层面可用性的问题,如果存在代码故障问题,或者因为特殊原因用户流量激增,导致我们的服务扛不住了,那在跨机房集群的情况下一样会导致系统服务不可用;
  • 所以我们就需要提前做好了「容错隔离」的一些方案,比如限流、熔断等等,用上这些方法还是可以保证一部分服务或者一部分用户的访问是正常。

2.3 容错隔离的方案有哪些

超时

  • 这是简单的容错方式。指在服务之间调用时,设置一个主动超时时间作为时间阈值;
  • 超过了这个时间阈值后,如果“被依赖的服务”还没有返回数据的话,“调用者”就主动放弃,防止因“被依赖的服务”故障无法返回结果造成服务无法处理请求的问题。

限流

  • 顾名思义,就是限制最大流量。系统能提供的最大并发有限,同时来的请求又太多,服务无法处理请求,就只好排队限流;
  • 类比就跟生活中去景点排队买票、去商场吃饭排队等号的道理;
  • 常见的限流算法有:计算器限流、漏桶算法、令牌漏桶算法、集群限流算法。

降级

  • 与限流类似,一样是流量太多,系统服务不过来。这个时候可将不是那么重要的功能模块进行降级处理,停止服务,这样可以释放出更多的资源供给核心功能的去用;
  • 同时还可以对用户分层处理,优先处理重要用户的请求,比如 VIP 收费用户等;
  • 例如淘宝双十一活动会对订单查询服务降级来保证购买下单服务的可用性。

延迟异步处理

  • 这个方式是指设置一个流量缓冲池,所有的请求先进入这个缓冲池等待处理;
  • 真正的服务处理方按顺序从这个缓冲池中取出请求依次处理,这种方式可以减轻后端服务的压力,但是对用户来说体验上有延迟;
  • 技术上一般会采用消息队列的异步和削峰作用来实现。

熔断

  • 可以理解成就像电闸的保险丝一样,当流量过大或者错误率过大的时候,保险丝就熔断了,链路就断开了,不提供服务了;
  • 当流量恢复正常,或者后端服务稳定了,保险丝会自动街上(熔断闭合),服务又可以正常提供了。这是一种很好的保护后端微服务的一种方式;
  • 熔断技术中有个很重要的概念就是:断路器,可以参考下图:

图片

断路器其实就是一个状态机原理,有三种状态:

  • Closed:闭合状态,也就是正常状态;
  • Open:开启状态,也就是当后端服务出故障后链路断开,不提供服务的状态;
  • Half-Open:半闭合状态,就是允许一小部分流量进行尝试,尝试后发现服务正常就转为Closed状态,服务依旧不正常就转为Open状态。

2.4 开源容错隔离应用

Hystrix 原理图

图片

  • 当我们使用了 Hystrix 之后,请求会被封装到 HystrixCommand 中,这也就是第一步;
  • 第二步就是开始执行请求,Hystrix 支持同步执行(图中 .execute 方法)、异步执行(图中 .queue 方法)和响应式执行(图中 .observer);
  • 第三步判断缓存,如果存在与缓存中,则直接返回缓存结果;
  • 如果不在缓存中,则走第四步,判断断路器的状态是否为开启,如果是开启状态,也就是短路了,那就进行失败返回,跳到第八步;
  • 第八步需要对失败返回的处理也需要再做一次判断,要么正常失败返回,返回相应信息,要么根本没有实现失败返回的处理逻辑,就直接报错;
  • 如果断路器不是开启状态,那请求就继续走,进行第五步,判断线程、队列是否满了。如果满了,那么同样跳到第八步;
  • 如果线程没满,则走到第六步,执行远程调用逻辑,然后判断远程调用是否成功。调用发生异常了就挑到第八步,调用正常就挑到第九步正常返回信息。
  • 图中的第七步,非常牛逼的一个模块,是来收集 Hystrix 流程中的各种信息来对系统做监控判断的。

Hystrix 断路器的原理图

图片

  • Hystrix 通过滑动时间窗口算法来实现断路器的,是以秒为单位的滑桶式统计。它总共包含 10 个桶,每秒钟一个生成一个新的桶,往前推移,旧的桶就废弃掉;
  • 每一个桶中记录了所有服务调用的状态,调用次数、是否成功等信息,断路器的开关就是把这 10 个桶进行聚合计算后,来判断当前是应该开启还是闭合的。

3. 微服务的访问安全

3.1 什么是访问安全

  • 访问安全就是要保证符合系统要求的请求才可以正常访问服务来响应数据,避免非正常服务对系统进行攻击和破坏;
  • 微服务会进行服务的拆分,服务也会随业务分为内部服务和外部服务,同时需要保证哪些服务可以直接访问,哪些不可以;
  • 总的来说,访问安全本质上就是要作为访问的认证。

3.2 传统单机服务的访问安全机制

传统单体应用的访问示意图:

图片

  • 在应用服务器里面,会有一个 auth 模块(一般采用过滤器来实现)。当有客户端请求进来时,所有的请求都必须首先经过这个 auth 来做身份验证,验证通过后,才将请求发到后面的业务逻辑;
  • 通常客户端在第一次请求的时候会带上身份校验信息(用户名和密码),auth 模块在验证信息无误后,就会返回 Cookie 存到客户端,之后每次客户端只需要在请求中携带 Cookie 来访问,而 auth 模块也只需要校验 Cookie 的合法性后决定是否放行;
  • 可见,在传统单体应用中的安全架构还是蛮简单的,对外也只有一个入口,通过 auth 校验后,内部的用户信息都是内存、线程传递,逻辑并不是复杂,所以风险也在可控范围内。

3.3 微服务如何实现访问安全

在微服务架构下,一般有以下三种方案:

  • 网关鉴权模式(API Gateway)
  • 服务自主鉴权模式
  • API Token模式(OAuth2.0)

3.3.1 网关鉴权模式(API Gateway)

图片

  • 通过上图可见,因为在微服务的最前端一般会有一个 API 网关模块(API Gateway)。所有外部请求访问微服务集群时,都会首先通过这个API Gateway。可以在这个模块里部署 auth 逻辑,实现统一集中鉴权。鉴权通过后,再把请求转发给后端各个服务;
  • 这种模式的优点就是,由 API Gateway 集中处理了鉴权的逻辑,使得后端各微服务节点自身逻辑就简单了,只需要关注业务逻辑,无需关注安全性事宜;
  • 这个模式的问题就是,API Gateway 适用于身份验证和简单的路径授权(基于 URL),对于复杂数据、角色的授权访问权限,通过 API Gateway 很难去灵活的控制。毕竟这些逻辑都是存在后端服务上的,并非存储在 API Gateway 里。

3.3.2 服务自主鉴权模式

图片

  • 服务自主鉴权就是指不通过前端的 API Gateway 来控制,而是由后端的每一个微服务节点自己去鉴权;
  • 它的优点就是可以由更为灵活的访问授权策略,并且相当于微服务节点完全无状态化了。同时还可以避免 API Gateway 中 auth 模块的性能瓶颈;
  • 缺点就是由于每一个微服务都自主鉴权,当一个请求要经过多个微服务节点时,会进行重复鉴权,增加了很多额外的性能开销。

3.3.3 API Token 模式(OAuth2.0)

图片

如图,这是一种采用基于令牌 Token 的授权方式。在这个模式下,是由授权服务器(图中 Authorization Server)、API 网关(图中 API Gateway)、内部的微服务节点几个模块组成。

流程如下:

  1. 客户端应用首先使用账号密码或者其它身份信息去访问授权服务器(Authorization Server)获取 访问令牌(Access Token);

  2. 拿到访问令牌(Access Token)后带着它再去访问API网关(图中 API Gateway),API Gateway 自己是无法判断这个 Access Token 是否合法,所以走第 3 步;

  3. API Gateway 去调用 Authorization Server 校验 Access Token 的合法性;

  4. 如果验证完 Access Token 是合法的,那 API Gateway 就将 Access Token 换成 JWT 令牌返回;

    注意:此处也可以不换成 JWT,而是直接返回原 Access Token。但是换成 JWT 更好,因为 Access Token 是一串不可读无意义的字符串,每次验证 Access Token 是否合法都需要去访问 Authorization Server 才知道。但是 JWT 令牌是一个包含 JSON 对象,有用户信息和其它数据的一个字符串,后面微服务节点拿到 JWT 之后,自己就可以做校验,减少了交互次数。

  5. API Gateway 有了JWT之后,就将请求向后端微服务节点进行转发,同时会带上这个 JWT;

  6. 微服务节点收到请求后,读取里面的 JWT,然后通过加密算法验证这个 JWT,验证通过后,就处理请求逻辑。

    这里面就使用到了 OAuth2.0 的原理,不过这只是 OAuth2.0 各类模式中的一种。

3.4 OAuth2.0 的访问安全 

3.4.1 什么是 OAuth2.0

OAuth2.0 是一种访问授权协议框架。它是基于 Token 令牌的授权方式,在不暴露用户密码的情况下,使应用方能够获取到用户数据的访问权限。

例如:你开发了一个视频网站,可以采用第三方微信登陆,那么只要用户在微信上对这个网站授权了,那这个网站就可以在无需用户密码的情况下获取用户在微信上的头像。

OAuth2.0 的流程如下图:

图片

3.4.2 OAuth2.0 主要名词解释

  • 资源服务器:用户数据、资源存放的地方,在微服务架构中,服务就是资源服务器。在上面的例子中,微信头像存放的服务就是资源服务器;
  • 资源拥有者:是指用户,资源的拥有人。在上面的例子中某个微信头像的用户就是资源拥有者;
  • 授权服务器:是一个用来验证用户身份并颁发令牌的服务器;
  • 客户端应用:想要访问用户受保护资源的客户端、Web应用。在上面的例子中的视频网站就是客户端应用;
  • 访问令牌:Access Token,授予对资源服务器的访问权限额度令牌;
  • 刷新令牌:客户端应用用于获取新的 Access Token 的一种令牌;
  • 客户凭证:用户的账号密码,用于在 授权服务器 进行验证用户身份的凭证。

3.4.3 OAuth2.0 有四种授权模式

授权码(Authorization Code)

授权码模式是指客户端应用先去申请一个授权码,然后再拿着这个授权码去获取令牌的模式。这也是目前最为常用的一种模式,安全性比较高,适用于我们常用的前后端分离项目。通过前端跳转的方式去访问授权服务器获取授权码,然后后端再用这个授权码访问授权服务器以获取访问令牌。

工作流程图

图片

第一步,客户端的前端页面(图中 UserAgent)将用户跳转到授权服务器(Authorization Server)里进行授权,授权完成后,返回授权码(Authorization Code)。

第二步,客户端的后端服务(图中 Client)携带授权码(Authorization Code)去访问 授权服务器,然后获得正式的访问令牌(Access Token)。

面的前端和后端分别做不同的逻辑,前端接触不到 Access Token,保证了 Access Token 的安全性。

简化模式(Implicit)

  • 简化模式是在项目是一个纯前端应用,在没有后端的情况下,采用的一种模式;
  • 因为这种方式令牌是直接存在前端的,所以非常不安全,因此令牌的有限期设置就不能太长。

工作流程图

图片

第一步,应用(纯前端的应用)将用户跳转到授权服务器(Authorization Server)里进行授权。授权完成后,授权服务器直接将 Access Token 返回给 前端应用,令牌存储在前端页面。

第二步,应用(纯前端的应用)携带访问令牌(Access Token)去访问资源,获取资源。

在整个过程中,虽然令牌是在前端 URL 中直接传递,但令牌不是放在 HTTP 协议中 URL 参数字段中的,而是放在 URL 锚点里。因为锚点数据不会被浏览器发到服务器,因此有一定的安全保障。

用户名密码(Resource Owner Credentials)

图片

这种方式最容易理解了,直接使用用户名、密码作为授权方式去访问授权服务器,从而获取 Access Token。

这个方式因为需要用户给出自己的密码,所以非常的不安全性。一般仅在客户端应用与授权服务器、资源服务器是归属统一公司、团队,互相非常信任的情况下采用。

客户端凭证(Client Credentials)

图片

这是适用于服务器间通信的场景。客户端应用拿一个用户凭证去找授权服务器获取Access Token。

4. 容器技术

4.1 为什么需要容器技术

传统的 PaaS 技术虽然也可以一键将本地应用部署到云上,并且也是采用隔离环境的形式去部署,但是其兼容性非常的不好。

因为其主要原理就是将本地应用程序和启停脚本一同打包,然后上传到云服务器上,然后再在云服务器里通过脚本启动这个应用程序。

这样的做法,看起来很理想。但是在实际情况下,由于本地与云端的环境差异,导致上传到云端的应用运行的时候经常各种报错,需要各种修改配置和参数来做兼容服务环境。甚至在项目迭代过程中不同的版本代码都需要重新去做适配,非常耗费精力。

然而以Docker为代表的容器技术却通过一个小创新完美的解决了这个问题。

在 Docker 的方案中,它不仅打包了本地应用程序,而且还将本地环境(操作系统的一部分)也打包了,组成一个叫做「 Docker镜像 」的文件包。所以这个「 Docker镜像 」就包含了应用运行所需的全部依赖,我们可以直接基于这个「 Docker镜像 」在本地进行开发与测试,完成之后,再直接将这个「 Docker镜像 」一键上传到云端运行即可。

Docker 实现了本地与云端的环境完全一致,做到了真正的一次开发随处运行,避免了类似“我在本地正常运行,传到云端就不可以了”的说辞。

4.2 什么是容器

先来看一下容器与虚拟机的对比区别:

图片

虚拟机是在宿主机上基于 Hypervisor 软件虚拟出一套操作系统所需的硬件设备,再在这些虚拟硬件上安装操作系统 Guest OS,然后不同的应用程序就可以运行在不同的 Guest OS 上,应用之间也就相互独立、资源隔离。但是由于需要 Hypervisor 来创建虚拟机,且每个虚拟机里需要完整的运行一套操作系统 Guest OS,因此这个方式会带来很多额外资源的开销。

Docker容器中却没有 Hypervisor 这一层,虽然它需要在宿主机中运行 Docker Engine,但它的原理却完全不同于 Hypervisor,它并没有虚拟出硬件设备,更没有独立部署全套的操作系统 Guest OS。

Docker容器没有那么复杂的实现原理,它其实就是一个普通进程而已,只不过它是一种经过特殊处理过的普通进程。我们启动容器的时候(docker run …),Docker Engine 只不过是启动了一个进程,这个进程就运行着我们容器里的应用。

但 Docker Engine 对这个进程做了一些特殊处理,通过这些特殊处理之后,这个进程所看到的外部环境就不再是宿主机的环境(它看不到宿主机中的其它进程了,以为自己是当前操作系统唯一一个进程),并且 Docker Engine 还对这个进程所使用得资源进行了限制,防止它对宿主机资源的无限使用。

对比下来就是,容器比虚拟机更加轻量级,花销也更小,更好地利用好主机的资源。

4.3 容器如何做到资源隔离和限制

Docker 容器对这个进程的隔离主要采用两个核心技术点 Namespace 和 Cgroups。

图片

总结来说就是,Namespace 为容器进程开辟隔离进程,Cgroups 限制容器进程之间抢夺资源,从此保证了容器之间独立运行和隔离。

Namespace 技术

Namespace 是 Linux 操作系统默认提供的 API,包括 PID Namespace、Mount Namespace、IPC Namespace、Network Namespace 等。

以 PID Namespace 举例,它的功能是可以让我们在创建进程的时候,告诉Linux系统,我们要创建的进程需要一个新的独立的进程空间,并且这个进程在这个新的进程空间里的 PID=1。也就是说这个进程只看得到这个新进程空间里的东西,看不到外面宿主机环境里的东西,也看不到其它进程。

不过这只是一个虚拟空间,事实上这个进程在宿主机里 PID 该是啥还是啥,没有变化,只不过在这个进程空间里,该进程以为自己的 PID=1。

打个比方,就像是一个班级,每个人在这个班里都有一个编号。班里有 90 人,然后来了一位新同学,那他在班里的编号就是 91。可是老师为了给这位同学特别照顾,所以在班里开辟了一块独立的看不到外面的小隔间,并告诉这个同学他的编号是 1。由于这位同学在这个小空间里隔离着,所以他真的以为自己就是班上的第一位同学且编号为 1。当然了,事实上这位同学在班上的编号依然是 91。

Network Namespace 也是类似的技术原理,让这个进程只能看到当前 Namespace 空间里的网络设备,看不到宿主机真实情况。同理,其它 Mount、IPC 等 Namespace 也是这样。Namespace 技术其实就是修改了应用进程的视觉范围,但应用进程的本质却没有变化。

不过,Docker容器里虽然带有一部分操作系统(文件系统相关),但它并没有内核,因此多个容器之间是共用宿主机的操作系统内核的。这一点与虚拟机的原理是完全不一样的。

Cgroups 技术

Cgroup 全称是 Control Group,其功能就是限制进程组所使用的最大资源(这些资源可以是 CPU、内存、磁盘等等)。

既然 Namespace 技术 只能改变一下进程组的视觉范围,并不能真实的对资源做出限制。那么为了防止容器(进程)之间互相抢资源,甚至某个容器把宿主机资源全部用完导致其它容器也宕掉的情况发生。因此,必须采用 Cgroup 技术对容器的资源进行限制。

Cgroup 技术也是 Linux 默认提供的功能,在 Linux 系统的 /sys/fs/cgroup 下面有一些子目录 cpu、memory 等。Cgroup 技术提供的功能就是可以基于这些目录实现对这些资源进行限制。

例如,在 /sys/fs/cgroup/cpu 下面创建一个 dockerContainer 子目录,系统就会自动在这个新建的目录下面生成一些配置文件,这些配置文件就是用来控制资源使用量的。例如可以在这些配置文件里面设置某个进程ID对CPU的最大使用率。

Cgroup 对其它内存、磁盘等资源也是采用同样原理做限制。

4.4 什么是容器镜像

一个基础的容器镜像其实就是一个 rootfs,它包含操作系统的文件系统(文件和目录),但并不包含操作系统的内核。rootfs 是在容器里根目录上挂载的一个全新的文件系统,此文件系统与宿主机的文件系统无关,是一个完全独立的,用于给容器进行提供环境的文件系统。

对于一个Docker容器而言,需要基于 pivot_root 指令,将容器内的系统根目录切换到rootfs上。这样,有了这个 rootfs,容器就能够为进程构建出一个完整的文件系统,且实现了与宿主机的环境隔离。也正是有了rootfs,才能实现基于容器的本地应用与云端应用运行环境的一致。

另外,为了方便镜像的复用,Docker 在镜像中引入了层(Layer)的概念,可以将不同的镜像一层一层的迭在一起。这样,如果我们要做一个新的镜像,就可以基于之前已经做好的某个镜像的基础上继续做。

图片

如上图,这个例子中最底层是操作系统引导。往上一层就是基础镜像层(Linux 的文件系统),再往上就是我们需要的各种应用镜像,Docker 会把这些镜像联合挂载在一个挂载点上,这些镜像层都是只读的。只有最上面的容器层是可读可写的。

这种分层的方案其实是基于联合文件系统 UnionFS(Union File System)的技术实现的。它可以将不同的目录全部挂载在同一个目录下。

举个例子,假如有文件夹 test1 和 test2 ,这两个文件夹里面有相同的文件,也有不同的文件。然后我们可以采用联合挂载的方式,将这两个文件夹挂载到 test3 上,那么 test3 目录里就有了 test1 和 test2 的所有文件(相同的文件有去重,不同的文件都保留)。

这个原理应用在 Docker 镜像中。比如有 2 个同学,同学 A 已经做好了一个基于 Linux 的 Java 环境的镜像,同学 S 想搭建一个 Java Web 环境,那么他就不必再去做 Java 环境的镜像了,可以直接基于同学 A 的镜像在上面增加 Tomcat 后生成新镜像即可。

4.5 容器技术在微服务的实践

随着微服务的流行,容器技术也相应的被大家重视起来。容器技术主要解决了以下两个问题。

环境一致性问题

例如 Java 的 jar/war 包部署会依赖于环境的问题(操着系统的版本,JDK 版本问题)。

镜像部署问题

例如 Java、Ruby、NodeJS 等等的发布系统是不一样的,每个环境都得很麻烦的部署一遍,采用 Docker 镜像,就屏蔽了这类问题。

部署实践

下图是 Docker 容器部署的一个完整过程:

图片

更重要的是,拥有如此多服务的集群环境迁移、复制也非常轻松,只需选择好各服务对应的 Docker 服务镜像、配置好相互之间访问地址就能很快搭建出一份完全一样的新集群。

4.6 容器调度

目前基于容器的调度平台有 Kubernetes(K8S)、Mesos、Omega。

总结

本文主要介绍了微服务架构下的服务监控、容错隔离、访问安全以及结合容器技术实现服务发布和部署。初窥了微服务架构的模块,相信对微服务架构会有所帮助。再深入就需要有针对性的技术实践才能加深了解。

转自:Joyo,

链接:joyohub.com/micro-server-pro/

[转]一张“无脑”清单告诉你分布式系统代码有多复杂

https://mp.weixin.qq.com/s/abohPG3ACJD34BsGszvrNA

开篇

微服务架构在当今的软件工程领域被广泛采用。同时,采用分布式架构的组织也发现需要考虑分布式故障的附加复杂性,而这种复杂性往往超出实际业务逻辑。

虽然分布式计算的谬误是有据可查的,但对于组织而言并不是一件容易的事情。因此,构建大规模、可靠的分布式系统架构就成为一个难题。作为推论,当我们将网络交互的复杂性引入其中时,在原先非分布式系统中看起来很好的代码就有可能成为一个大问题。

在生产代码中摸爬滚打几年后,遭遇了各种故障模式并且发现导致故障的根源之后,我逐渐能够识别一些更常见的故障模式。由于不同公司以及使用不同的语言堆栈之间存在差异(取决于内部基础设施和工具的成熟度),但是可以从产生问题的原因中总结出一些具有共性的经验。

下面就是我从这些经验中总结出来的一些代码审查指南,这个指南可以形成一份清单,并用来审查分布式环境中与系统间通信相关的代码。虽然这份清单上提到的问题并不适用所有情况,但它们覆盖了代码审查的基本面,可以按照这个清单将问题走查一遍,在此过程中标记缺失的项目以供进一步讨论,利用这种方式发现系统中的问题是非常行之有效的。从这个意义上来说,可以通过这个“无脑清单”来发现大多数问题。

图片

如何调用远程系统

1、当远程系统发生故障时会发生什么?

无论系统设计的多么谨慎,它都会出现故障 – 这是在生产中被印证的事实。故障的发生可能源于代码错误,基础设施问题,流量激增,系统疏于管理等,总之结果是引发故障。调用者如何处理故障将决定整个架构的弹性和健壮性。

定义错误处理路径:必须在代码中明确错误处理路径,而不是让系统在最终用户面前崩溃。这里需要向用户明确指出错误,例如:设计良好的错误页面、带有错误信息的异常日志,以及带有回退机制的断路器等。

制定恢复计划:考虑代码中的每一次远程交互,并弄清楚如何恢复被中断的工作。思考如下价格问题:工作流程是否需要有状态才能从故障点触发?是否将所有失败的有效请求发布到重试队列/数据库表,并在远程系统恢复时重试请求?是否有脚本来比较两个系统的数据库并以某种方式使它们同步?在部署系统之前,是否有一个明确的系统的恢复计划?

2、当远程系统变慢时会发生什么?

这种情况比彻底失败更难办,因为我们不知道远程系统是否在工作。因此需要检查以下事项从而处理这种情况。如果我们使用类似 Istio 的服务网格技术,其中一些问题可以轻松搞定而不需要修改应用程序代码。即便如此,我们也应该关注这些问题。

为远程系统调用设置超时:这包括远程 API 调用、事件发布和数据库调用的超时时间。我在很多代码中发现过这个问题,因此需要检查远程系统是否设置了合理的超时时间,从而避免该系统在无响应时调用者因为等待而浪费资源的情况发生。

超时重试:网络和系统并不是100%可靠的,重试对于系统恢复是非常必要的。重试机制会消除系统交互中的许多“问题”。如果可能,在重试中使用某种补偿机制(固定的、指数的)。在重试机制中添加一点抖动(这里的抖动可以理解为随机重试,例如设置随机的重试时间3-5s重试一次,避免所有调用者一起不断地对被调用者进行重试,导致被调用者的负载增大),这样做可以给被调用系统一些喘息的空间,通过能够保证调用者在负载下获得更好的调用成功率。重试的另一面是幂等性,我们将在本文后面介绍。

使用断路器:一些应用程序并没有预先打包这个功能,但我看到公司内部会编写自己的包装器。如果你有这个需求,一定要实现它,对断路器的投入会让你获益。它会提供明确的框架来定义错误情况下的回退策略。

不要把超时当作请求失败来处理——超时不是失败,而是一种不确定的场景,应该通过一种处理方式来应对这种不确定性。因此需要建立明确的处理机制,允许系统在发生超时的情况下进行同步。处理机制可以是简单的协调脚本,也可以是有状态的工作流,或者是通过死信队列(消息被拒绝、消息TTL过期、队列达到最大长度)实现。

不要在事务中调用远程系统——当远程系统访问速度变慢时,依旧会长时间保持数据库连接,如果访问持续而因为速度的问题一直无法完成系统的访问,会导致数据库的连接也无法释放,也就将数据库连接用完,最终造成系统中断的后果。

使用智能批处理:如果处理大量数据请求,可以逐个进行批量远程调用(API 调用、数据库读取)从而消除网络开销。每个批量处理的量越大,整体延迟就会越大,可能失败的工作单元也会越多。因此需要针对性能和容错性优化批量大小。

如何面对调用方请求

所有 API 必须保证幂等性:幂等性是为了实现调用方 API 的超时重试功能。只有 API 能够支持安全重试且不会有副作用时,调用者才能安心使用重试功能。这里的 API 是指同步 API 和任何消息传递接口——调用者可能会发布两次相同的消息(或者代理可能会发送两次)给到该 API。

图片

明确定义响应时间和吞吐量 SLA 以及遵守定义的规则:在分布式系统中,快速失败比让调用者等待要好得多。诚然,吞吐量 SLA 很难实现(分布式速率限制一个难题),但我们需要确保 SLA 在主动呼叫失败时做好准备。另一个重要方面是了解下游系统的响应时间,以确定系统最快的速度。

定义和限制批处理 API:如果公开批处理 API ,则应明确定义最大批处理的数量,这个数量需要受到 SLA 的限制,也就是需要遵守 SLA 的规则定义。

预先考虑可观察性:可观察性意味着能够分析系统的行为,而无需通过查看API或组件的内部来实现。预先考虑你关心的系统指标以及需要收集的数据,帮助你回答以前未提出的问题。再对系统进行检测并获得这些数据。执行此操作的一个强大机制是识别系统的域模型,当域中发生某个事件时进行发布事件的操作。(例如收到请求id 123,返回请求 123 的响应——注意如何使用这两个“域”事件会导出一个称为“响应时间”的新指标。将原始数据转换到预先确定的聚合中)。

一般性原则

尽量使用缓存:网络变化无常,因此尽可能多地使用缓存,并不断讲最新的数据保存其中。当然,有可能会使用远程缓存机制(例如,Redis服务器运行在单独的服务器上),但至少通过缓存的方式可以将数据带入控制域并减少系统的负载。

考虑单元故障:如果一个 API 或一条消息代表多个工作单元(批处理),那么需要思考单元故障意味着什么?如果有效载荷都失败一次意味着什么?又或者单个单元独立成功或失败意味着什么?部分成功呢,API 是否响应成功或失败代码?

这里的意思是一个 API 调用多个工作单元,这里的工作单元可以是一个组件或者是一个 API 。有可能在调用多个工作单元的时候,其中一个工作单元失败了,或者有的工作单元成功了,这个时候作为最外层调用这些工作单元的 API 来说要考虑好是成功还是失败,如果失败如何返回失败信息。

在系统边缘隔离外部域对象:不允许以重用的名义在系统中使用其他系统的域对象。这将会加剧我们的系统与其他系统的实体建模的耦合,在其他系统发生更改时,我们的系统都会进行大量重构。我们应该始终构建自己的实体表示并将外部有效负载转换为此我们系统内的模式,然后我们的系统中使用它。

安全性

在每个边缘清理输入:在分布式环境中,系统的任何部分都可能受到损害(从安全角度来看)。因此,在系统边界处会对进入系统的数据进行“消毒”处理,这里有一个假设就是这些进入系统的数据有可能不是干净或安全的。

永远不要提交凭证(Credentials):永远不要将凭证(数据库用户名/密码或 API 密钥)提交到代码库。虽然提交凭证到代码库对于某些人来说是常规操作,但我们需要摒弃这种陋习。始终遵守“凭证必须始终从外部(有安全存储保证)加载到系统”的规则。

译者介绍

崔皓,51CTO社区编辑,资深架构师,拥有18年的软件开发和架构经验,10年分布式架构经验。曾任惠普技术专家。乐于分享,撰写了很多热门技术文章,阅读量超过60万。《分布式架构原理与实践》作者。

原文链接:
https://kislayverma.com/programming/code-review-checklist-for-distributed-systems/

[转]Unicode、UTF-8、UTF-16,终于懂了

https://mp.weixin.qq.com/s/dIuTohi2CLkmOe1skGVf4w

计算机起源于美国,上个世纪,他们对英语字符与二进制位之间的关系做了统一规定,并制定了一套字符编码规则,这套编码规则被称为ASCII编码

ASCII 编码一共定义了128个字符的编码规则,用七位二进制表示 ( 0x00 – 0x7F ), 这些字符组成的集合就叫做 ASCII 字符集

随着计算机的普及,在不同的地区和国家又出现了很多字符编码,比如: 大陆的 GB2312、港台的 BIG5, 日本的 Shift JIS等等

由于字符编码不同,计算机在不同国家之间的交流变得很困难,经常会出现乱码的问题,比如:对于同一个二进制数据,不同的编码会解析出不同的字符

当互联网迅猛发展,地域限制打破之后,人们迫切的希望有一种统一的规则, 对所有国家和地区的字符进行编码,于是 Unicode 就出现了

Unicode 简介

Unicode 是国际标准字符集,它将世界各种语言的每个字符定义一个唯一的编码,以满足跨语言、跨平台的文本信息转换

Unicode 字符集的编码范围是 0x0000 – 0x10FFFF , 可以容纳一百多万个字符, 每个字符都有一个独一无二的编码,也即每个字符都有一个二进制数值和它对应,这里的二进制数值也叫 码点 , 比如:汉字 “中” 的 码点是 0x4E2D, 大写字母 A 的码点是 0x41, 具体字符对应的 Unicode 编码可以查询 Unicode字符编码表

字符集和字符编码

字符集是很多个字符的集合,例如 GB2312 是简体中文的字符集,它收录了六千多个常用的简体汉字及一些符号,数字,拼音等字符

字符编码是 字符集的一种实现方式,把字符集中的字符映射为特定的字节或字节序列,它是一种规则

比如:Unicode 只是字符集,UTF-8、UTF-16、UTF-32 才是真正的字符编码规则

Unicode 字符存储

Unicode 是一个符号集, 它只规定了每个符号的二进制值,但是符号具体如何存储它并没有规定

前面提到, Unicode 字符集的编码范围是 0x0000 – 0x10FFFF,因此需要 1 到 3 个字节来表示

那么,对于三个字节的 Unicode字符,计算机怎么知道它表示的是一个字符而不是三个字符呢 ?

如果所有字符都用三个字节表示,那么对于那些一个字节就能表示的字符来说,有两个字节是无意义的,对于存储来说,这是极大的浪费,假如 , 一个普通的文本, 大部分字符都只需一个字节就能表示,现在如果需要三个字节才能表示,文本的大小会大出三倍左右

因此,Unicode 出现了多种存储方式,常见的有 UTF-8、UTF-16、UTF-32,它们分别用不同的二进制格式来表示 Unicode 字符

UTF-8、UTF-16、UTF-32 中的 “UTF” 是 “Unicode Transformation Format” 的缩写,意思是”Unicode 转换格式”,后面的数 字表明至少使用多少个比特位来存储字符, 比如:UTF-8 最少需要8个比特位也就是一个字节来存储,对应的, UTF-16 和 UTF-32 分别需要最少 2 个字节 和 4 个字节来存储

UTF-8 编码

UTF-8: 是一种变长字符编码,被定义为将码点编码为 1 至 4 个字节,具体取决于码点数值中有效二进制位的数量

UTF-8 的编码规则:

  1. 对于单字节的符号,字节的第一位设为 0,后面 7 位为这个符号的 Unicode 码。因此对于英语字母,UTF-8 编码和 ASCII 码是相同的, 所以 UTF-8 能兼容 ASCII 编码,这也是互联网普遍采用 UTF-8 的原因之一
  1. 对于 n 字节的符号( n > 1),第一个字节的前 n 位都设为 1,第 n + 1 位设为 0,后面字节的前两位一律设为 10 。剩下的没有提及的二进制位,全部为这个符号的 Unicode 码

下表是Unicode编码对应UTF-8需要的字节数量以及编码格式

Unicode编码范围(16进制) UTF-8编码方式(二进制)
000000 – 00007F 0xxxxxxx ASCII码
000080 – 0007FF 110xxxxx 10xxxxxx
000800 – 00FFFF 1110xxxx 10xxxxxx 10xxxxxx
01 0000 – 10 FFFF 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

表格中第一列是Unicode编码的范围,第二列是对应UTF-8编码方式,其中红色的二进制 “1” 和 “0” 是固定的前缀, 字母 x 表示可用编码的二进制位

根据上面表格,要解析 UTF-8 编码就很简单了,如果一个字节第一位是 0 ,则这个字节就是一个单独的字符,如果第一位是 1 ,则连续有多少个 1 ,就表示当前字符占用多少个字节

下面以 “中” 字 为例来说明 UTF-8 的编码,具体的步骤如下图, 为了便于说明,图中左边加了 1,2,3,4 的步骤编号

图片

首先查询 “中” 字的 Unicode 码 0x4E2D, 转成二进制, 总共有 16 个二进制位, 具体如上图 步骤1 所示

通过前面的 Unicode 编码和 UTF-8 编码的表格知道,Unicode 码 0x4E2D 对应 000800 – 00FFFF 的范围,所以, “中” 字的 UTF-8 编码 需要 3 个字节,即格式是 1110xxxx 10xxxxxx 10xxxxxx

然后从 “中” 字的最后一个二进制位开始,按照从后向前的顺序依次填入格式中的 x 字符,多出的二进制补为 0, 具体如上图 步骤2、步骤3 所示

于是,就得到了 “中” 的 UTF-8 编码是 11100100 10111000 10101101, 转换成十六进制就是 0xE4B8AD, 具体如上图 步骤4 所示

UTF-16 编码

UTF-16 也是一种变长字符编码, 这种编码方式比较特殊, 它将字符编码成 2 字节 或者 4 字节

具体的编码规则如下:

  1. 对于 Unicode 码小于 0x10000 的字符, 使用 2 个字节存储,并且是直接存储 Unicode 码,不用进行编码转换
  1. 对于 Unicode 码在 0x10000 和 0x10FFFF 之间的字符,使用 4 个字节存储,这 4 个字节分成前后两部分,每个部分各两个字节,其中,前面两个字节的前 6 位二进制固定为 110110,后面两个字节的前 6 位二进制固定为 110111, 前后部分各剩余 10 位二进制表示符号的 Unicode 码 减去 0x10000 的结果
  1. 大于 0x10FFFF 的 Unicode 码无法用 UTF-16 编码

下表是Unicode编码对应UTF-16编码格式

Unicode编码范围(16进制) 具体Unicode码(二进制) UTF-16编码方式(二进制) 字节
0000 0000 – 0000 FFFF xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx 2
0001 0000 – 0010 FFFF yy yyyyyyyy xx xxxxxxxx 110110yy yyyyyyyy 110111xx xxxxxxxx 4

表格中第一列是Unicode编码的范围,第二列是 具体Unicode码的二进制 ( 第二行的第二列表示的是 Unicode 码 减去 0x10000 后的二进制 ) , 第三列是对应UTF-16编码方式,其中红色的二进制 “1” 和 “0” 是固定的前缀, 字母 x 和 y 表示可用编码的二进制位, 第四列表示 编码占用的字节数

前面提到过,”中” 字的 Unicode 码是 4E2D, 它小于 0x10000,根据表格可知,它的 UTF-16 编码占两个字节,并且和 Unicode 码相同,所以 “中” 字的 UTF-16 编码为 4E2D

我从 Unicode字符表网站 找了一个老的南阿拉伯字母, 它的 Unicode 码是: 0x10A6F , 可以访问 https://unicode-table.com/cn/10A6F/ 查看字符的说明, Unicode 码对应的字符如下图所示

图片

下面以这个 老的南阿拉伯字母的 Unicode 码 0x10A6F 为例来说明 UTF-16 4 字节的编码,具体步骤如下,为了便于说明,图中左边加了 1,2,3,4 、5的步骤编号

图片

首先把 Unicode 码 0x10A6F 转成二进制, 对应上图的 步骤 1

然后把 Unicode 码 0x10A6F 减去 0x10000, 结果为 0xA6F 并把这个值转成二进制 00 00000010 10 01101111,对应上图的 步骤 2

然后 从二进制 00 00000010 10 01101111 的最后一个二进制为开始,按照从后向前的顺序依次填入格式中的 x 和 y 字符,多出的二进制补为 0, 对应上图的 步骤 3、 步骤 4

于是,就计算出了 Unicode 码 0x10A6F 的 UTF-16 编码是 11011000 00000010 11011110 01101111 , 转换成十六进制就是 0xD802DE6F, 对应上图的 步骤 5

UTF-32 编码

UTF-32 是固定长度的编码,始终占用 4 个字节,足以容纳所有的 Unicode 字符,所以直接存储 Unicode 码即可,不需要任何编码转换。虽然浪费了空间,但提高了效率。

UTF-8、UTF-16、UTF-32 之间如何转换

前面介绍过,UTF-8、UTF-16、UTF-32 是 Unicode 码表示成不同的二进制格式的编码规则,同样,通过这三种编码的二进制表示,也能获得对应的 Unicode 码,有了字符的 Unicode 码,按照上面介绍的 UTF-8、UTF-16、UTF-32 的编码方法 就能转换成任一种编码了

UTF 字节序

最小编码单元是多字节才会有字节序的问题存在,UTF-8 最小编码单元是一字节,所以 它是没有字节序的问题,UTF-16 最小编码单元是 2 个字节,在解析一个 UTF-16 字符之前,需要知道每个编码单元的字节序

比如:前面提到过,”中” 字的 Unicode 码是 4E2D, “ⵎ” 字符的 Unicode 码是 2D4E, 当我们收到一个 UTF-16 字节流 4E2D 时,计算机如何识别它表示的是字符 “中” 还是 字符 “ⵎ” 呢 ?

所以,对于多字节的编码单元,需要有一个标记显式的告诉计算机,按照什么样的顺序解析字符,也就是字节序,字节序分为 大端字节序 和 小端字节序

小端字节序简写为 LE( Little-Endian ), 表示 低位字节在前,高位字节在后, 高位字节保存在内存的高地址端,而低位字节保存在内存的低地址端

大端字节序简写为 BE( Big-Endian ), 表示 高位字节在前,低位字节在后,高位字节保存在内存的低地址端,低位字节保存在在内存的高地址端

下面以 0x4E2D 为例来说明大端和小端,具体参见下图:

图片

数据是从高位字节到低位字节显示的,这也更符合人们阅读数据的习惯,而内存地址是从低地址向高地址增加

所以,字符0x4E2D 数据的高位字节是 4E,低位字节是 2D

按照大端字节序的高位字节保存内存低地址端的规则,4E 保存到低内存地址 0x10001 上,2D 则保存到高内存地址 0x10002 上

对于小端字节序,则正好相反,数据的高位字节保存到内存的高地址端,低位字节保存到内存低地址端的,所以 4E 保存到高内存地址 0x10002 上,2D 则保存到低内存地址 0x10001 上

BOM

BOM 是 byte-order mark 的缩写,是 “字节序标记” 的意思, 它常被用来当做标识文件是以 UTF-8、UTF-16 或 UTF-32 编码的标记

在 Unicode 编码中有一个叫做 “零宽度非换行空格” 的字符 ( ZERO WIDTH NO-BREAK SPACE ), 用字符 FEFF 来表示

对于 UTF-16 ,如果接收到以 FEFF 开头的字节流, 就表明是大端字节序,如果接收到 FFFE, 就表明字节流 是小端字节序

UTF-8 没有字节序问题,上述字符只是用来标识它是 UTF-8 文件,而不是用来说明字节顺序的。”零宽度非换行空格” 字符 的 UTF-8 编码是 EF BB BF, 所以如果接收到以 EF BB BF 开头的字节流,就知道这是UTF-8 文件

下面的表格列出了不同 UTF 格式的固定文件头

UTF编码 固定文件头
UTF-8 EF BB BF
UTF-16LE FF FE
UTF-16BE FE FF
UTF-32LE FF FE 00 00
UTF-32BE 00 00 FE FF

根据上面的 固定文件头,下面列出了 “中” 字在文件中的存储 ( 包含文件头 )

编码 固定文件头
Unicode 编码 0X004E2D
UTF-8 EF BB BF 4E 2D
UTF-16BE FE FF 4E 2D
UTF-16LE FF FE 2D 4E
UTF-32BE 00 00 FE FF 00 00 4E 2D
UTF-32LE FF FE 00 00 2D 4E 00 00

常见的字符编码的问题

  • Redis 中文key的显示

有时候我们需要向redis中写入含有中文的数据,然后在查看数据,但是会看到一些其他的字符,而不是我们写入的中文

图片

上图中,我们向redis 写入了一个 “中” 字,通过 get 命令查看的时候无法显示我们写入的 “中” 字

这时候加一个 –raw 参数,重新启动 redis-cli 即可,也即 执行 redis-cli –raw 命令启动redis客户端,具体的如下图所示

图片

  • MySQL 中的 utf8 和 utf8mb4

MySQL 中的 “utf8” 实际上不是真正的 UTF-8, “utf8” 只支持每个字符最多 3 个字节, 对于超过 3 个字节的字符就会出错, 而真正的 UTF-8 至少要支持 4 个字节

MySQL 中的 “utf8mb4” 才是真正的 UTF-8

下面以 test 表为例来说明, 表结构如下:

mysql> show create table test\G
*************************** 1. row ***************************
       Table: test
Create Table: CREATE TABLE `test` (
  `name` char(32) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8
1 row in set (0.00 sec)

向 test 表分别插入 “中” 字 和 Unicode 码为 0x10A6F 的字符,这个字符需要从 https://unicode-table.com/cn/10A6F/ 直接复制到 MySQL 控制台上,手工输入会无效,具体的执行结果如下图:

图片

从上图可以看出,插入 “中” 字 成功,插入 0x10A6F 字符失败,错误提示无效的字符串,\xF0\X90\XA9\xAF 正是 0x10A6F 字符的 UTF-8 编码,占用 4 个字节, 因为 MySQL 的 utf8 编码最多只支持 3 个字节,所以插入会失败

把 test 表的字符集改成 utf8mb4 , 排序规则 改成 utf8bm4_unicode_ci, 具体如下图所示:

图片

字符集和排序方式修改之后,再次插入 0x10A6F 字符, 结果是成功的,具体执行结果如下图所示

图片

上图中,set names utf8mb4 是为了测试方便,临时修改当前会话的字符集,以便保持和 服务器一致,实际解决这个问题需要修改 my.cnf 配置中 服务器和客户端的字符集

小结

本文从字符编码的历史介绍了 Unicode 出现的原因,接着介绍了 Unicode 字符集中 三种不同的编码方式:UTF-8、UTF-16、UTF-32 以及它们的的编码方法,紧接着介绍了 字节序、BOM ,最后讲到了字符集在 MySQL 和 Redis 应用中常见的问题以及解决方案 ,更多关于 Unicode 的介绍请参考 Unicode 的 RFC 文档。

Elasticsearch 7.X 安装并使用

ES 7目前安装相当简单,这里简单记录下步骤。

1,https://www.elastic.co/cn/downloads/elasticsearch 官网按照版本下载安装包,比如 LINUX X86_64shaasc

2,解压后基本可以直接使用,但是这里需要简单配置下

vim elasticsearch.yml

开启network,path,cluster.nam node.name相关配置

3,启动/bin/elasticsearch

二 同步数据

这里需要将历史数据同步,采用

https://github.com/elasticsearch-dump/elasticsearch-dump

即可,不过需要更新最新的nodejs,

npm i -g n

n latest

然后同步数据即可!

 

Elasticsearch 如何做到快速检索 – 倒排索引的秘密

“All problems in computer science can be solved by another level of indirection.”

– David J. Wheeler

“计算机世界就是 trade-off 的艺术”

摘抄了一篇比较好的,上面图和方法在很多博客都被引用,但是相对组织的较好。

一、前言

最近接触的几个项目都使用到了 Elasticsearch (以下简称 ES ) 来存储数据和对数据进行搜索分析,就对 ES 进行了一些学习。本文整理自我自己的一次技术分享。

本文不会关注 ES 里面的分布式技术、相关 API 的使用,而是专注分享下 ”ES 如何快速检索“ 这个主题上面。这个也是我在学习之前对 ES 最感兴趣的部分。


本文大致包括以下内容:

  • 关于搜索

    • 传统关系型数据库和 ES 的差别

    • 搜索引擎原理

  • 细究倒排索引

    • 倒排索引具体是个什么样子的(posting list -> term dic -> term index)

    • 关于 postings list 的一些巧技 (FOR、Roaring Bitmaps)

    • 如何快速做联合查询?

二、关于搜索

先设想一个关于搜索的场景,假设我们要搜索一首诗句内容中带“前”字的古诗,

用 传统关系型数据库和 ES 实现会有什么差别?

如果用像 MySQL 这样的 RDBMS 来存储古诗的话,我们应该会去使用这样的 SQL 去查询

select name from poems where content like "%前%";
复制代码

这种我们称为顺序扫描法,需要遍历所有的记录进行匹配。

不但效率低,而且不符合我们搜索时的期望,比如我们在搜索“ABCD”这样的关键词时,通常还希望看到”A”,”AB”,”CD”,“ABC”的搜索结果。

于是乎就有了专业的搜索引擎,比如我们今天的主角 — ES。

搜索引擎原理

搜索引擎的搜索原理简单概括的话可以分为这么几步,

  • 内容爬取,停顿词过滤

    比如一些无用的像”的”,“了”之类的语气词/连接词

  • 内容分词,提取关键词

  • 根据关键词建立倒排索引

  • 用户输入关键词进行搜索

这里我们就引出了一个概念,也是我们今天的要剖析的重点 – 倒排索引。也是 ES 的核心知识点。

如果你了解 ES 应该知道,ES 可以说是对 Lucene 的一个封装,里面关于倒排索引的实现就是通过 lucene 这个 jar 包提供的 API 实现的,所以下面讲的关于倒排索引的内容实际上都是 lucene 里面的内容。

三、倒排索引

首先我们还不能忘了我们之前提的搜索需求,先看下建立倒排索引之后,我们上述的查询需求会变成什么样子,

这样我们一输入“前”,借助倒排索引就可以直接定位到符合查询条件的古诗。

当然这只是一个很大白话的形式来描述倒排索引的简要工作原理。在 ES 中,这个倒排索引是具体是个什么样的,怎么存储的等等,这些才是倒排索引的精华内容。

1. 几个概念

在进入下文之前,先描述几个前置概念。

term

关键词这个东西是我自己的讲法,在 ES 中,关键词被称为 term

postings list

还是用上面的例子,{静夜思, 望庐山瀑布}是 “前” 这个 term 所对应列表。在 ES 中,这些被描述为所有包含特定 term 文档的 id 的集合。由于整型数字 integer 可以被高效压缩的特质,integer 是最适合放在 postings list 作为文档的唯一标识的,ES 会对这些存入的文档进行处理,转化成一个唯一的整型 id。

再说下这个 id 的范围,在存储数据的时候,在每一个 shard 里面,ES 会将数据存入不同的 segment,这是一个比 shard 更小的分片单位,这些 segment 会定期合并。在每一个 segment 里面都会保存最多 2^31 个文档,每个文档被分配一个唯一的 id,从0(2^31)-1

相关的名词都是 ES 官方文档给的描述,后面参考材料中都可以找到出处。

2. 索引内部结构

上面所描述的倒排索引,仅仅是一个很粗糙的模型。真的要在实际生产中使用,当然还差的很远。

在实际生产场景中,比如 ES 最常用的日志分析,日志内容进行分词之后,可以得到多少的 term?

那么如何快速的在海量 term 中查询到对应的 term 呢?遍历一遍显然是不现实的。

term dictionary

于是乎就有了 term dictionary,ES 为了能快速查找到 term,将所有的 term 排了一个序,二分法查找。是不是感觉有点眼熟,这不就是 MySQL 的索引方式的,直接用 B+树建立索引词典指向被索引的数据。

term index

但是问题又来了,你觉得 Term Dictionary 应该放在哪里?肯定是放在内存里面吧?磁盘 io 那么慢。就像 MySQL 索引就是存在内存里面了。

但是如果把整个 term dictionary 放在内存里面会有什么后果呢?

内存爆了…

别忘了,ES 默认可是会对全部 text 字段进行索引,必然会消耗巨大的内存,为此 ES 针对索引进行了深度的优化。在保证执行效率的同时,尽量缩减内存空间的占用。

于是乎就有了 term index

Term index 从数据结构上分类算是一个“Trie 树”,也就是我们常说的字典树。这是一种专门处理字符串匹配的数据结构,用来解决在一组字符串集合中快速查找某个字符串的问题。

这棵树不会包含所有的 term,它包含的是 term 的一些前缀(这也是字典树的使用场景,公共前缀)。通过 term index 可以快速地定位到 term dictionary 的某个 offset,然后从这个位置再往后顺序查找。就想右边这个图所表示的。(怎么样,像不像我们查英文字典,我们定位 S 开头的第一个单词,或者定位到 Sh 开头的第一个单词,然后再往后顺序查询)

lucene 在这里还做了两点优化,一是 term dictionary 在磁盘上面是分 block 保存的,一个 block 内部利用公共前缀压缩,比如都是 Ab 开头的单词就可以把 Ab 省去。二是 term index 在内存中是以 FST(finite state transducers)的数据结构保存的。

FST 有两个优点:

  • 空间占用小。通过对词典中单词前缀和后缀的重复利用,压缩了存储空间
  • 查询速度快。O(len(str)) 的查询时间复杂度。

FST 的理论比较复杂,本文不细讲

延伸阅读:https://www.shenyanchao.cn/blog/2018/12/04/lucene-fst/

OK,现在我们能得到 lucene 倒排索引大致是个什么样子的了。

四、关于 postings list 的一些巧技

在实际使用中,postings list 还需要解决几个痛点,

  • postings list 如果不进行压缩,会非常占用磁盘空间,
  • 联合查询下,如何快速求交并集(intersections and unions)

对于如何压缩,可能会有人觉得没有必要,”posting list 不是已经只存储文档 id 了吗?还需要压缩?”,但是如果在 posting list 有百万个 doc id 的情况,压缩就显得很有必要了。(比如按照朝代查询古诗?),至于为啥需要求交并集,ES 是专门用来搜索的,肯定会有很多联合查询的需求吧 (AND、OR)。

按照上面的思路,我们先将如何压缩。

1. 压缩

Frame of Reference

在 lucene 中,要求 postings lists 都要是有序的整形数组。这样就带来了一个很好的好处,可以通过 增量编码(delta-encode)这种方式进行压缩。

比如现在有 id 列表 [73, 300, 302, 332, 343, 372],转化成每一个 id 相对于前一个 id 的增量值(第一个 id 的前一个 id 默认是 0,增量就是它自己)列表是[73, 227, 2, 30, 11, 29]在这个新的列表里面,所有的 id 都是小于 255 的,所以每个 id 只需要一个字节存储

实际上 ES 会做的更加精细,

它会把所有的文档分成很多个 block,每个 block 正好包含 256 个文档,然后单独对每个文档进行增量编码,计算出存储这个 block 里面所有文档最多需要多少位来保存每个 id,并且把这个位数作为头信息(header)放在每个 block 的前面。这个技术叫 Frame of Reference

上图也是来自于 ES 官方博客中的一个示例(假设每个 block 只有 3 个文件而不是 256)。

FOR 的步骤可以总结为:

进过最后的位压缩之后,整型数组的类型从固定大小 (8,16,32,64 位)4 种类型,扩展到了[1-64] 位共 64 种类型。

通过以上的方式可以极大的节省 posting list 的空间消耗,提高查询性能。不过 ES 为了提高 filter 过滤器查询的性能,还做了更多的工作,那就是缓存

Roaring Bitmaps (for filter cache)

在 ES 中,可以使用 filters 来优化查询,filter 查询只处理文档是否匹配与否,不涉及文档评分操作,查询的结果可以被缓存。

对于 filter 查询,es 提供了 filter cache 这种特殊的缓存,filter cache 用来存储 filters 得到的结果集。缓存 filters 不需要太多的内存,它只保留一种信息,即哪些文档与 filter 相匹配。同时它可以由其它的查询复用,极大地提升了查询的性能。

我们上面提到的 Frame Of Reference 压缩算法对于 postings list 来说效果很好,但对于需要存储在内存中的 filter cache 等不太合适。

filter cache 会存储那些经常使用的数据,针对 filter 的缓存就是为了加速处理效率,对压缩算法要求更高。

对于这类 postings list,ES 采用不一样的压缩方式。那么让我们一步步来。

首先我们知道 postings list 是 Integer 数组,具有压缩空间。

假设有这么一个数组,我们第一个压缩的思路是什么?用位的方式来表示,每个文档对应其中的一位,也就是我们常说的位图,bitmap。

它经常被作为索引用在数据库、查询引擎和搜索引擎中,并且位操作(如 and 求交集、or 求并集)之间可以并行,效率更好。

但是,位图有个很明显的缺点,不管业务中实际的元素基数有多少,它占用的内存空间都恒定不变。也就是说不适用于稀疏存储。业内对于稀疏位图也有很多成熟的压缩方案,lucene 采用的就是roaring bitmaps

我这里用简单的方式描述一下这个压缩过程是怎么样,

将 doc id 拆成高 16 位,低 16 位。对高位进行聚合 (以高位做 key,value 为有相同高位的所有低位数组),根据低位的数据量 (不同高位聚合出的低位数组长度不相同),使用不同的 container(数据结构) 存储。

  • len<4096 ArrayContainer 直接存值

  • len>=4096 BitmapContainer 使用 bitmap 存储

分界线的来源:value 的最大总数是为2^16=65536. 假设以 bitmap 方式存储需要 65536bit=8kb,而直接存值的方式,一个值 2 byte,4K 个总共需要2byte*4K=8kb。所以当 value 总量 <4k 时,使用直接存值的方式更节省空间。

空间压缩主要体现在:

  • 高位聚合 (假设数据中有 100w 个高位相同的值,原先需要 100w2byte,现在只要 12byte)

  • 低位压缩

缺点就在于位操作的速度相对于原生的 bitmap 会有影响。

这就是 trade-off 呀。平衡的艺术。

2. 联合查询

讲完了压缩,我们再来讲讲联合查询。

先讲简单的,如果查询有 filter cache,那就是直接拿 filter cache 来做计算,也就是说位图来做 AND 或者 OR 的计算。

如果查询的 filter 没有缓存,那么就用 skip list 的方式去遍历磁盘上的 postings list。

以上是三个 posting list。我们现在需要把它们用 AND 的关系合并,得出 posting list 的交集。首先选择最短的 posting list,逐个在另外两个 posting list 中查找看是否存在,最后得到交集的结果。遍历的过程可以跳过一些元素,比如我们遍历到绿色的 13 的时候,就可以跳过蓝色的 3 了,因为 3 比 13 要小。

用 skip list 还会带来一个好处,还记得前面说的吗,postings list 在磁盘里面是采用 FOR 的编码方式存储的

会把所有的文档分成很多个 block,每个 block 正好包含 256 个文档,然后单独对每个文档进行增量编码,计算出存储这个 block 里面所有文档最多需要多少位来保存每个 id,并且把这个位数作为头信息(header)放在每个 block 的前面。

因为这个 FOR 的编码是有解压缩成本的。利用 skip list,除了跳过了遍历的成本,也跳过了解压缩这些压缩过的 block 的过程,从而节省了 cpu

五、总结

下面我们来做一个技术总结(感觉有点王刚老师的味道😂)

  • 为了能够快速定位到目标文档,ES 使用倒排索引技术来优化搜索速度,虽然空间消耗比较大,但是搜索性能提高十分显著。
  • 为了能够在数量巨大的 terms 中快速定位到某一个 term,同时节约对内存的使用和减少磁盘 io 的读取,lucene 使用 “term index -> term dictionary -> postings list” 的倒排索引结构,通过 FST 压缩放入内存,进一步提高搜索效率。
  • 为了减少 postings list 的磁盘消耗,lucene 使用了 FOR(Frame of Reference)技术压缩,带来的压缩效果十分明显。
  • ES 的 filter 语句采用了 Roaring Bitmap 技术来缓存搜索结果,保证高频 filter 查询速度的同时降低存储空间消耗。
  • 在联合查询时,在有 filter cache 的情况下,会直接利用位图的原生特性快速求交并集得到联合查询结果,否则使用 skip list 对多个 postings list 求交并集,跳过遍历成本并且节省部分数据的解压缩 cpu 成本

Elasticsearch 的索引思路

将磁盘里的东西尽量搬进内存,减少磁盘随机读取次数 (同时也利用磁盘顺序读特性),结合各种压缩算法,用及其苛刻的态度使用内存。

所以,对于使用 Elasticsearch 进行索引时需要注意:

  • 不需要索引的字段,一定要明确定义出来,因为默认是自动建索引的
  • 同样的道理,对于 String 类型的字段,不需要 analysis 的也需要明确定义出来,因为默认也是会 analysis 的
  • 选择有规律的 ID 很重要,随机性太大的 ID(比如 Java 的 UUID) 不利于查询

最后说一下,技术选型永远伴随着业务场景的考量,每种数据库都有自己要解决的问题(或者说擅长的领域),对应的就有自己的数据结构,而不同的使用场景和数据结构,需要用不同的索引,才能起到最大化加快查询的目的。

这篇文章讲的虽是 Lucene 如何实现倒排索引,如何精打细算每一块内存、磁盘空间、如何用诡谲的位运算加快处理速度,但往高处思考,再类比一下 MySQL,你就会发现,虽然都是索引,但是实现起来,截然不同。笼统的来说,b-tree 索引是为写入优化的索引结构。当我们不需要支持快速的更新的时候,可以用预先排序等方式换取更小的存储空间,更快的检索速度等好处,其代价就是更新慢,就像 ES。

希望本篇文章能给你带来一些收获~

参考文档

  • https://www.elastic.co/cn/blog/frame-of-reference-and-roaring-bitmaps
  • https://www.elastic.co/cn/blog/found-elasticsearch-from-the-bottom-up
  • http://blog.mikemccandless.com/2014/05/choosing-fast-unique-identifier-uuid.html
  • https://www.infoq.cn/article/database-timestamp-02
  • https://zhuanlan.zhihu.com/p/137574234

作者:Richard_Yi
链接:https://juejin.im/post/6889020742366920712
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

golang调试工具Delve

Devle是一个非常棒的golang 调试工具,支持多种调试方式,直接运行调试,或者attach到一个正在运行中的golang程序,进行调试。

线上golang服务出现问题时,Devle是必不少的在线调试工具,如果使用docker,也可以把Devle打进docker镜像里,调试代码。

  安装Devle

安装Devle非常简单,直接运行go  get 即可:

go get -u github.com/derekparker/delve/cmd/dlv

如果你的go版本为1.5请先设置环境变量GO15VENDOREXPERIMENT=1再运行go get。我的go版本为1.10,不用设置。

   使用Devle调试golang服务

先写一个简单的web服务,然后使用Devle来进行调试。

在$GOPATH/src/github.com/mytest 文件夹下创建main.go

复制代码
 1 package main
 2 
 3 import (
 4     "fmt"
 5     "log"
 6     "net/http"
 7     "os"
 8 )
 9 
10 const port  = "8000"
11 
12 func main() {
13     http.HandleFunc("/hi", hi)
14 
15     fmt.Println("runing on port: " + port)
16     log.Fatal(http.ListenAndServe(":" + port, nil))
17 }
18 
19 func hi(w http.ResponseWriter, r *http.Request) {
20     hostName, _ := os.Hostname()
21     fmt.Fprintf(w, "HostName: %s", hostName)
22 }
复制代码

简单吧,一个运行在8000端口上的web服务,访问 hi会返回机器的名称。上面代码的行号是很有用的,等会我们打断点的时候会用到。

使用Delve运行我们的main.go

dlv debug ./main.go

 

可以输入help来看一下帮助文档

很简单的一些命令

我们先打在main方法上打一个断点:

b main.main

然后运行c 来运行到断点,

 

 

在func li  里打一个断点,我们可以使用

b main.hi

或者使用   “文件:行号”来打断点

b /home/goworkspace/src/github.com/mytest/main.go:20

 

 

现在执行continue 让服务跑起来。访问一下我们的服务,看hi方法会不会停下来。

curl localhost:8000/hi

看到了没,在19号停下来了。

输入 n 回车,执行到下一行

输入s 回车,单步执行

输入 print(别名p)输出变量信息

输入 args 打印出所有的方法参数信息

输入 locals 打印所有的本地变量

 

其他的命令我就不在这里给大家演示了,自己动动手试一下。

  使用Delve附加到运行的golang服务进行调试

先编译一下我们的main.go然后去行main

go build main.go

./main

 

然后使用Delve附加到我们的项目上,先看一下我们的项目的pid

ps aux|grep main
dlv attach 29260

 

在hi方法里打断点,然后执行c来等待断点的执行。

b /home/goworkspace/src/github.com/mytest/main.go:20

 

访问我们的服务器,看一下断点会不会被执行

curl localhost:8000/hi

 

断点执行了。然后调试你的代码吧!

gpt-2安装和使用

关于gpt2的安装,官网git上有详细的方法。这里提供一种docker安装的方式。

https://github.com/openai/gpt-2/blob/master/DEVELOPERS.md

Docker Installation

Build the Dockerfile and tag the created image as gpt-2:

docker build --tag gpt-2 -f Dockerfile.gpu . # or Dockerfile.cpu

Start an interactive bash session from the gpt-2 docker image.

You can opt to use the --runtime=nvidia flag if you have access to a NVIDIA GPU and a valid install of nvidia-docker 2.0.

docker run --runtime=nvidia -it gpt-2 bash
安装完成后,如果报:

AttributeError: module ‘tensorflow’ has no attribute ‘xxxx’的错误,重新卸载和安装tensorflow即可。:

1,pip uninstall tensorflow

2,pip install tensorflow

测试结果

测试结果

在简短的说明文档里,有三种示例运行模式,第一种是无限制乱说模式,

python3 src/generate_unconditional_samples.py | tee samples

这段命令行的意思是,| 这个通道符号之前是主要的调用代码,无限制生成文本片段,tee 是 linux 下的一个命令行工具,用于将产生的文本重定向到文件 samples 中。第二种运行模式是用几个参数改变无限制生成文本的特性,

python3 src/generate_unconditional_samples.py --top_k 40 --temperature 0.7 | tee samples

其中最重要的那个参数可能是 temperature (温度), 这个参数调节输出的随机性,温度越高,随机性越强,越低,出现copy训练语料里说过的话的几率就越大。具体实现应该是从 logits 到字典中字的 index 计算的 softmax 函数中加入温度参数 T,

[公式]

第三种运行模式, 交互式按照用户提示生成文本片段,这种模式最有意思,也是我们要调戏的对象。调用代码是,

python3 src/interactive_conditional_samples.py

下面是一个例子,效果不太好

Model prompt >>> Snow White and Transformers
======================================== SAMPLE 1 ========================================
: The Last Knight as the Bundles. Released in Japan in 1999 as The Predatory White, this manga Jamie appears to be made by Santa Claus.

In Friends: Firstly we find out that Jamie has successfully become an adult, despite being struggling with bankruptcy and all that, it seems. While young and seemingly used to working alone, he falls for the woman who treats her much better than other characters.

But wait no more, she fights after Jamie and soon falls in love with Daisy. It doesn’t take that long to make out even more love back in Nicholas Cage connection but it’s written in cadence so it’s not really that long back.

On Nicholas Cage, Jamie is finally embracing the role that he was actually engaged in as a teenager: He watched the Brown Brothers The Shawshank Redemption and that made him Titanite. It wasn’t until the same film that he completely succumbed to cowardice, rather in the way he said “Hey, this is why I have a skirt now, I’m not sure if it does any good. I’m starting to wonder if this is a animalistic mature woman. However, to serve men, they’re way too similar. It’s like a beautiful woman rather than the same bitch that had nothing to do with the man. Which being true, I’d never normally argue for this.”

Appear in Halloween 6th as a Regent of the Wardrobe, now standing in he saves Henry from being devastated by the title to Baby Belle.

Jamie in Space solves crises, saving Captain Race from mutated beasts throwing the latter super mutants into space.

In Uprising, in juxtaposing the monsters running through space to the drill that takes Charlie as the leader, Jamie sidesteps the danger through cryptic symbols by blending human emotions into his scattered messages—I ♡ A good way to pray. Boo Boo!

In Guys Who Wear Mini Hats, Jamie rescues rescuers: Jay McLean and Robbie Bar-Sotto.

In Moombaine’s Boarding Line of Asteroids, Jamie is found trying to figure out what’s going on behind the underground ship Icarus when Kitty Pope comes back. Initially the plot hinges on if “I cannot” being a signal to Cygnus with tubes coming in. However, one scene simply made sense, putting pressure on to Kitty by going back to Icarus. So why is the second handled the more foolish way.

Jamie is now raising Kitty Papillon up as a guide

[转]使用 Dockerfile 定制镜像

原文地址:https://yeasy.gitbooks.io/docker_practice/image/build.html

从刚才的 docker commit 的学习中,我们可以了解到,镜像的定制实际上就是定制每一层所添加的配置、文件。如果我们可以把每一层修改、安装、构建、操作的命令都写入一个脚本,用这个脚本来构建、定制镜像,那么之前提及的无法重复的问题、镜像构建透明性的问题、体积的问题就都会解决。这个脚本就是 Dockerfile。

Dockerfile 是一个文本文件,其内包含了一条条的 指令(Instruction),每一条指令构建一层,因此每一条指令的内容,就是描述该层应当如何构建。

还以之前定制 nginx 镜像为例,这次我们使用 Dockerfile 来定制。

在一个空白目录中,建立一个文本文件,并命名为 Dockerfile

$ mkdir mynginx
$ cd mynginx
$ touch Dockerfile

其内容为:

FROM nginx
RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html

这个 Dockerfile 很简单,一共就两行。涉及到了两条指令,FROM 和 RUN

FROM 指定基础镜像

所谓定制镜像,那一定是以一个镜像为基础,在其上进行定制。就像我们之前运行了一个 nginx 镜像的容器,再进行修改一样,基础镜像是必须指定的。而 FROM 就是指定 基础镜像,因此一个 Dockerfile 中 FROM 是必备的指令,并且必须是第一条指令。

在 Docker Hub 上有非常多的高质量的官方镜像,有可以直接拿来使用的服务类的镜像,如 nginxredismongomysqlhttpdphptomcat 等;也有一些方便开发、构建、运行各种语言应用的镜像,如 nodeopenjdkpythonrubygolang 等。可以在其中寻找一个最符合我们最终目标的镜像为基础镜像进行定制。

如果没有找到对应服务的镜像,官方镜像中还提供了一些更为基础的操作系统镜像,如 ubuntudebiancentosfedoraalpine 等,这些操作系统的软件库为我们提供了更广阔的扩展空间。

除了选择现有镜像为基础镜像外,Docker 还存在一个特殊的镜像,名为 scratch。这个镜像是虚拟的概念,并不实际存在,它表示一个空白的镜像。

FROM scratch
...

如果你以 scratch 为基础镜像的话,意味着你不以任何镜像为基础,接下来所写的指令将作为镜像第一层开始存在。

不以任何系统为基础,直接将可执行文件复制进镜像的做法并不罕见,比如 swarmetcd。对于 Linux 下静态编译的程序来说,并不需要有操作系统提供运行时支持,所需的一切库都已经在可执行文件里了,因此直接 FROM scratch 会让镜像体积更加小巧。使用 Go 语言 开发的应用很多会使用这种方式来制作镜像,这也是为什么有人认为 Go 是特别适合容器微服务架构的语言的原因之一。

RUN 执行命令

RUN 指令是用来执行命令行命令的。由于命令行的强大能力,RUN 指令在定制镜像时是最常用的指令之一。其格式有两种:

  • shell 格式:RUN <命令>,就像直接在命令行中输入的命令一样。刚才写的 Dockerfile 中的 RUN 指令就是这种格式。
RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
  • exec 格式:RUN ["可执行文件", "参数1", "参数2"],这更像是函数调用中的格式。

既然 RUN 就像 Shell 脚本一样可以执行命令,那么我们是否就可以像 Shell 脚本一样把每个命令对应一个 RUN 呢?比如这样:

FROM debian:stretch

RUN apt-get update
RUN apt-get install -y gcc libc6-dev make wget
RUN wget -O redis.tar.gz "http://download.redis.io/releases/redis-5.0.3.tar.gz"
RUN mkdir -p /usr/src/redis
RUN tar -xzf redis.tar.gz -C /usr/src/redis --strip-components=1
RUN make -C /usr/src/redis
RUN make -C /usr/src/redis install

之前说过,Dockerfile 中每一个指令都会建立一层,RUN 也不例外。每一个 RUN 的行为,就和刚才我们手工建立镜像的过程一样:新建立一层,在其上执行这些命令,执行结束后,commit 这一层的修改,构成新的镜像。

而上面的这种写法,创建了 7 层镜像。这是完全没有意义的,而且很多运行时不需要的东西,都被装进了镜像里,比如编译环境、更新的软件包等等。结果就是产生非常臃肿、非常多层的镜像,不仅仅增加了构建部署的时间,也很容易出错。 这是很多初学 Docker 的人常犯的一个错误。

Union FS 是有最大层数限制的,比如 AUFS,曾经是最大不得超过 42 层,现在是不得超过 127 层。

上面的 Dockerfile 正确的写法应该是这样:

FROM debian:stretch

RUN buildDeps='gcc libc6-dev make wget' \
    && apt-get update \
    && apt-get install -y $buildDeps \
    && wget -O redis.tar.gz "http://download.redis.io/releases/redis-5.0.3.tar.gz" \
    && mkdir -p /usr/src/redis \
    && tar -xzf redis.tar.gz -C /usr/src/redis --strip-components=1 \
    && make -C /usr/src/redis \
    && make -C /usr/src/redis install \
    && rm -rf /var/lib/apt/lists/* \
    && rm redis.tar.gz \
    && rm -r /usr/src/redis \
    && apt-get purge -y --auto-remove $buildDeps

首先,之前所有的命令只有一个目的,就是编译、安装 redis 可执行文件。因此没有必要建立很多层,这只是一层的事情。因此,这里没有使用很多个 RUN 对一一对应不同的命令,而是仅仅使用一个 RUN 指令,并使用 && 将各个所需命令串联起来。将之前的 7 层,简化为了 1 层。在撰写 Dockerfile 的时候,要经常提醒自己,这并不是在写 Shell 脚本,而是在定义每一层该如何构建。

并且,这里为了格式化还进行了换行。Dockerfile 支持 Shell 类的行尾添加 \ 的命令换行方式,以及行首 # 进行注释的格式。良好的格式,比如换行、缩进、注释等,会让维护、排障更为容易,这是一个比较好的习惯。

此外,还可以看到这一组命令的最后添加了清理工作的命令,删除了为了编译构建所需要的软件,清理了所有下载、展开的文件,并且还清理了 apt 缓存文件。这是很重要的一步,我们之前说过,镜像是多层存储,每一层的东西并不会在下一层被删除,会一直跟随着镜像。因此镜像构建时,一定要确保每一层只添加真正需要添加的东西,任何无关的东西都应该清理掉。

很多人初学 Docker 制作出了很臃肿的镜像的原因之一,就是忘记了每一层构建的最后一定要清理掉无关文件。

构建镜像

好了,让我们再回到之前定制的 nginx 镜像的 Dockerfile 来。现在我们明白了这个 Dockerfile 的内容,那么让我们来构建这个镜像吧。

在 Dockerfile 文件所在目录执行:

$ docker build -t nginx:v3 .
Sending build context to Docker daemon 2.048 kB
Step 1 : FROM nginx
 ---> e43d811ce2f4
Step 2 : RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
 ---> Running in 9cdc27646c7b
 ---> 44aa4490ce2c
Removing intermediate container 9cdc27646c7b
Successfully built 44aa4490ce2c

从命令的输出结果中,我们可以清晰的看到镜像的构建过程。在 Step 2 中,如同我们之前所说的那样,RUN 指令启动了一个容器 9cdc27646c7b,执行了所要求的命令,并最后提交了这一层 44aa4490ce2c,随后删除了所用到的这个容器 9cdc27646c7b

这里我们使用了 docker build 命令进行镜像构建。其格式为:

docker build [选项] <上下文路径/URL/->

在这里我们指定了最终镜像的名称 -t nginx:v3,构建成功后,我们可以像之前运行 nginx:v2 那样来运行这个镜像,其结果会和 nginx:v2 一样。

镜像构建上下文(Context)

如果注意,会看到 docker build 命令最后有一个 .. 表示当前目录,而 Dockerfile 就在当前目录,因此不少初学者以为这个路径是在指定 Dockerfile 所在路径,这么理解其实是不准确的。如果对应上面的命令格式,你可能会发现,这是在指定 上下文路径。那么什么是上下文呢?

首先我们要理解 docker build 的工作原理。Docker 在运行时分为 Docker 引擎(也就是服务端守护进程)和客户端工具。Docker 的引擎提供了一组 REST API,被称为 Docker Remote API,而如 docker 命令这样的客户端工具,则是通过这组 API 与 Docker 引擎交互,从而完成各种功能。因此,虽然表面上我们好像是在本机执行各种 docker 功能,但实际上,一切都是使用的远程调用形式在服务端(Docker 引擎)完成。也因为这种 C/S 设计,让我们操作远程服务器的 Docker 引擎变得轻而易举。

当我们进行镜像构建的时候,并非所有定制都会通过 RUN 指令完成,经常会需要将一些本地文件复制进镜像,比如通过 COPY 指令、ADD 指令等。而 docker build 命令构建镜像,其实并非在本地构建,而是在服务端,也就是 Docker 引擎中构建的。那么在这种客户端/服务端的架构中,如何才能让服务端获得本地文件呢?

这就引入了上下文的概念。当构建的时候,用户会指定构建镜像上下文的路径,docker build 命令得知这个路径后,会将路径下的所有内容打包,然后上传给 Docker 引擎。这样 Docker 引擎收到这个上下文包后,展开就会获得构建镜像所需的一切文件。

如果在 Dockerfile 中这么写:

COPY ./package.json /app/

这并不是要复制执行 docker build 命令所在的目录下的 package.json,也不是复制 Dockerfile 所在目录下的 package.json,而是复制 上下文(context) 目录下的 package.json

因此,COPY 这类指令中的源文件的路径都是相对路径。这也是初学者经常会问的为什么 COPY ../package.json /app 或者 COPY /opt/xxxx /app 无法工作的原因,因为这些路径已经超出了上下文的范围,Docker 引擎无法获得这些位置的文件。如果真的需要那些文件,应该将它们复制到上下文目录中去。

现在就可以理解刚才的命令 docker build -t nginx:v3 . 中的这个 .,实际上是在指定上下文的目录,docker build 命令会将该目录下的内容打包交给 Docker 引擎以帮助构建镜像。

如果观察 docker build 输出,我们其实已经看到了这个发送上下文的过程:

$ docker build -t nginx:v3 .
Sending build context to Docker daemon 2.048 kB
...

理解构建上下文对于镜像构建是很重要的,避免犯一些不应该的错误。比如有些初学者在发现 COPY /opt/xxxx /app 不工作后,于是干脆将 Dockerfile 放到了硬盘根目录去构建,结果发现 docker build 执行后,在发送一个几十 GB 的东西,极为缓慢而且很容易构建失败。那是因为这种做法是在让 docker build 打包整个硬盘,这显然是使用错误。

一般来说,应该会将 Dockerfile 置于一个空目录下,或者项目根目录下。如果该目录下没有所需文件,那么应该把所需文件复制一份过来。如果目录下有些东西确实不希望构建时传给 Docker 引擎,那么可以用 .gitignore 一样的语法写一个 .dockerignore,该文件是用于剔除不需要作为上下文传递给 Docker 引擎的。

那么为什么会有人误以为 . 是指定 Dockerfile 所在目录呢?这是因为在默认情况下,如果不额外指定 Dockerfile 的话,会将上下文目录下的名为 Dockerfile 的文件作为 Dockerfile。

这只是默认行为,实际上 Dockerfile 的文件名并不要求必须为 Dockerfile,而且并不要求必须位于上下文目录中,比如可以用 -f ../Dockerfile.php 参数指定某个文件作为 Dockerfile

当然,一般大家习惯性的会使用默认的文件名 Dockerfile,以及会将其置于镜像构建上下文目录中。

其它 docker build 的用法

直接用 Git repo 进行构建

或许你已经注意到了,docker build 还支持从 URL 构建,比如可以直接从 Git repo 中构建:

$ docker build https://github.com/twang2218/gitlab-ce-zh.git#:11.1

Sending build context to Docker daemon 2.048 kB
Step 1 : FROM gitlab/gitlab-ce:11.1.0-ce.0
11.1.0-ce.0: Pulling from gitlab/gitlab-ce
aed15891ba52: Already exists
773ae8583d14: Already exists
...

这行命令指定了构建所需的 Git repo,并且指定默认的 master 分支,构建目录为 /11.1/,然后 Docker 就会自己去 git clone 这个项目、切换到指定分支、并进入到指定目录后开始构建。

用给定的 tar 压缩包构建

$ docker build http://server/context.tar.gz

如果所给出的 URL 不是个 Git repo,而是个 tar 压缩包,那么 Docker 引擎会下载这个包,并自动解压缩,以其作为上下文,开始构建。

从标准输入中读取 Dockerfile 进行构建

docker build - < Dockerfile

cat Dockerfile | docker build -

如果标准输入传入的是文本文件,则将其视为 Dockerfile,并开始构建。这种形式由于直接从标准输入中读取 Dockerfile 的内容,它没有上下文,因此不可以像其他方法那样可以将本地文件 COPY 进镜像之类的事情。

从标准输入中读取上下文压缩包进行构建

$ docker build - < context.tar.gz

如果发现标准输入的文件格式是 gzipbzip2 以及 xz 的话,将会使其为上下文压缩包,直接将其展开,将里面视为上下文,并开始构建。

nodejs使用

简单的说 Node.js 就是运行在服务端的 JavaScript。

Node.js 是一个基于Chrome JavaScript 运行时建立的一个平台。

Node.js是一个事件驱动I/O服务端JavaScript环境,基于Google的V8引擎,V8引擎执行Javascript的速度非常快,性能非常好。

当然现在真正用nodejs做为后端服务的情况并不多,很多大型公司还是使用java,golang,php和python.但是最近做网页爬虫的时候,总是有前端js加密,虽然可以看到js加密的方法,但是加密方式还是非常的不好翻译成其他语言。所以这里起一个nodejs的微服务专门做js的执行也是不错的。另外因为是基于v8引擎,现在有python,java,php等版本的v8js库,也可以使用。但是在兼容上来说,使用原生的nodejs还是非常简单。

一 安装

centos

sudo yum install nodejs

安装完成子自带了npm,这个类似于pip,可以方便安装扩展包。

二 使用

node xxx.js

简直不能再简单

三 启动http服务

因为这里要做路由等功能,使用express框架

npm install express

这里介绍下引入其他文件的办法

1,a.js


function a() {

return "hello world";

}

function b() {

return "b";

}

module.exports = {a, b};


var express = require("express");

var fun=require("./a");
var app = express();
app.get("/",function(req,res){
res.send("ok");
});
app.get("/hello",function(req,res){
res.contentType("json");

var str = fun.b();

console.log(str);
res.send(JSON.stringify({code:200,data:"success"}));
});
app.get("/user/:id",function(req,res){
var id = req.params.id;
var list = [];
list.push({id:101,name:"xxx",age:20});
list.push({id:102,name:"yyy",age:18});
res.contentType("json");
res.send(JSON.stringify(list[id]));
});
app.listen(8889,function(){
console.log("server running at http://127.0.0.1:8889/");
});

[go]json处理的一些总结

https://colobu.com/2017/06/21/json-tricks-in-Go/

本文是基于 Go 官方和 https://eager.io/blog/go-and-json/ 进行翻译整理的

JSON 是一种轻量级的数据交换格式,常用作前后端数据交换,Go 在 encoding/json 包中提供了对 JSON 的支持。

序列化

把 Go struct 序列化成 JSON 对象,Go 提供了 Marshal 方法,正如其含义所示表示编排序列化,函数签名如下:

1
func Marshal(v interface{}) ([]byte, error)

举例来说,比如下面的 Go struct:

1
2
3
4
5
type Message struct {
    Name string
    Body string
    Time int64
}

使用 Marshal 序列化:

1
2
3
m := Message{"Alice", "Hello", 1294706395881547000}
b, err := json.Marshal(m) 
fmt.Println(b) //{"Name":"Alice","Body":"Hello","Time":1294706395881547000}

在 Go 中并不是所有的类型都能进行序列化:

  • JSON object key 只支持 string
  • Channel、complex、function 等 type 无法进行序列化
  • 数据中如果存在循环引用,则不能进行序列化,因为序列化时会进行递归
  • Pointer 序列化之后是其指向的值或者是 nil

还需要注意的是:只有 struct 中支持导出的 field 才能被 JSON package 序列化,即首字母大写的 field

反序列化

反序列化函数是 Unmarshal ,其函数签名如下:

1
func Unmarshal(data []byte, v interface{}) error

如果要进行反序列化,我们首先需要创建一个可以接受序列化数据的 Go struct:

1
2
var m Message
err := json.Unmarshal(b, &m)

JSON 对象一般都是小写表示,Marshal 之后 JSON 对象的首字母依然是大写,如果序列化之后名称想要改变如何实现,答案就是 struct tags

Struct Tag

Struct tag 可以决定 Marshal 和 Unmarshal 函数如何序列化和反序列化数据。

指定 JSON filed name

JSON object 中的 name 一般都是小写,我们可以通过 struct tag 来实现:

1
2
3
type MyStruct struct {
    SomeField string `json:"some_field"`
}

SomeField 序列化之后会变成 some_field。

指定 field 是 empty 时的行为

使用 omitempty 可以告诉 Marshal 函数如果 field 的值是对应类型的 zero-value,那么序列化之后的 JSON object 中不包含此 field:

1
2
3
4
5
6
type MyStruct struct {
    SomeField string `json:"some_field,omitempty"`
}

m := MyStruct{}
b, err := json.Marshal(m) //{}

如果 SomeField == “” ,序列化之后的对象就是 {}

跳过 field

Struct tag “-” 表示跳过指定的 filed:

1
2
3
4
5
6
type MyStruct struct {
    SomeField string `json:"some_field"`
    Passwd string `json:"-"`
}
m := MyStruct{}
b, err := json.Marshal(m) //{"some_feild":""}

即序列化的时候不输出,这样可以有效保护需要保护的字段不被序列化。

反序列化任意 JSON 数据

默认的 JSON 只支持以下几种 Go 类型:

  • bool for JSON booleans
  • float64 for JSON numbers
  • string for JSON strings
  • nil for JSON null

在序列化之前如果不知道 JSON 数据格式,我们使用 interface{} 来存储。interface {} 的作用详见本博的其他文章。

有如下的数据格式:

1
b := []byte(`{"Name":"Wednesday","Age":6,"Parents":["Gomez","Morticia"]}`)

如果我们序列化之前不知道其数据格式,我们可以使用 interface{} 来存储我们的 decode 之后的数据:

1
2
var f interface{}
err := json.Unmarshal(b, &f)

反序列化之后 f 应该是像下面这样:

1
2
3
4
5
6
7
8
f = map[string]interface{}{
    "Name": "Wednesday",
    "Age":  6,
    "Parents": []interface{}{
        "Gomez",
        "Morticia",
    },
}

key 是 string,value 是存储在 interface{} 内的。想要获得 f 中的数据,我们首先需要进行 type assertion,然后通过 range 迭代获得 f 中所有的 key :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
m := f.(map[string]interface{})
for k, v := range m {
    switch vv := v.(type) {
    case string:
        fmt.Println(k, "is string", vv)
    case float64:
        fmt.Println(k, "is float64", vv)
    case []interface{}:
        fmt.Println(k, "is an array:")
        for i, u := range vv {
            fmt.Println(i, u)
        }
    default:
        fmt.Println(k, "is of a type I don't know how to handle")
    }
}

反序列化对 slice、map、pointer 的处理

我们定义一个 struct 继续对上面例子中的 b 进行反序列化:

1
2
3
4
5
6
7
8
type FamilyMember struct {
    Name    string
    Age     int
    Parents []string
}

var m FamilyMember
err := json.Unmarshal(b, &m)

这个例子是能够正常工作的,你一定也注意到了,struct 中包含一个 slice Parents ,slice 默认是 nil,之所以反序列化可以正常进行就是因为 Unmarshal 在序列化时进行了对 slice Parents 做了初始化,同理,对 map 和 pointer 都会做类似的工作,比如序列化如果 Pointer 不是 nil 首先进行 dereference 获得其指向的值,然后再进行序列化,反序列化时首先对 nil pointer 进行初始化

Stream JSON

除了 marshal 和 unmarshal 函数,Go 还提供了 Decoder 和 Encoder 对 stream JSON 进行处理,常见 request 中的 Body、文件等:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
jsonFile, err := os.Open("post.json")
if err != nil {
    fmt.Println("Error opening json file:", err)
    return
}

defer jsonFile.Close()
decoder := json.NewDecoder(jsonFile)
for {
    var post Post
    err := decoder.Decode(&post)
    if err == io.EOF {
        break
    }

    if err != nil {
        fmt.Println("error decoding json:", err)
        return
    }

    fmt.Println(post)
}

嵌入式 struct 的序列化

Go 支持对 nested struct 进行序列化和反序列化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
type App struct {
	Id string `json:"id"`
}

type Org struct {
	Name string `json:"name"`
}

type AppWithOrg struct {
	App
	Org
}

func main() {
	data := []byte(`
        {
            "id": "k34rAT4",
            "name": "My Awesome Org"
        }
    `)

	var b AppWithOrg

	json.Unmarshal(data, &b)
	fmt.Printf("%#v", b)

	a := AppWithOrg{
		App: App{
			Id: "k34rAT4",
		},
		Org: Org{
			Name: "My Awesome Org",
		},
	}
	data, _ = json.Marshal(a)
	fmt.Println(string(data))
}

Nested struct 虽然看起来有点怪异,有些时候它将非常有用。

自定义序列化函数

Go JSON package 中定了两个 Interface Marshaler 和 Unmarshaler ,实现这两个 Interface 可以让你定义的 type 支持序列化操作。

错误处理

总是记得检查序列或反序列化的错误,可以让你的程序更健壮,而不是在出错之后带着错误继续执行下去。

参考资料

  1. 临时忽略struct空字段
  2. 临时添加额外的字段
  3. 临时粘合两个struct
  4. 一个json切分成两个struct
  5. 临时改名struct的字段
  6. 用字符串传递数字
  7. 容忍字符串和数字互转
  8. 容忍空数组作为对象
  9. 使用 MarshalJSON支持time.Time
  10. 使用 RegisterTypeEncoder支持time.Time
  11. 使用 MarshalText支持非字符串作为key的map
  12. 使用 json.RawMessage
  13. 使用 json.Number
  14. 统一更改字段的命名风格
  15. 使用私有的字段
  16. 忽略掉一些字段
  17. 忽略掉一些字段2

taowenjson-iterator的作者。 序列化和反序列化需要处理JSON和struct的关系,其中会用到一些技巧。 原文 Golang 中使用 JSON 的小技巧是他的经验之谈,介绍了一些struct解析成json的技巧,以及 json-iterator 库的一些便利的处理。

有的时候上游传过来的字段是string类型的,但是我们却想用变成数字来使用。 本来用一个json:”,string” 就可以支持了,如果不知道golang的这些小技巧,就要大费周章了。

参考文章:http://attilaolah.eu/2014/09/10/json-and-struct-composition-in-go/

临时忽略struct空字段

1
2
3
4
5
type User struct {
Email string `json:”email”`
Password string `json:”password”`
// many more fields…
}

如果想临时忽略掉空Password字段,可以用omitempty:

1
2
3
4
5
6
json.Marshal(struct {
*User
Password bool `json:”password,omitempty”`
}{
User: user,
})

临时添加额外的字段

1
2
3
4
5
type User struct {
Email string `json:”email”`
Password string `json:”password”`
// many more fields…
}

临时忽略掉空Password字段,并且添加token字段

1
2
3
4
5
6
7
8
json.Marshal(struct {
*User
Token string `json:”token”`
Password bool `json:”password,omitempty”`
}{
User: user,
Token: token,
})

临时粘合两个struct

通过嵌入struct的方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
type BlogPost struct {
URL string `json:”url”`
Title string `json:”title”`
}
type Analytics struct {
Visitors int `json:”visitors”`
PageViews int `json:”page_views”`
}
json.Marshal(struct{
*BlogPost
*Analytics
}{post, analytics})

一个json切分成两个struct

1
2
3
4
5
6
7
8
9
json.Unmarshal([]byte(`{
“url”: “attila@attilaolah.eu”,
“title”: “Attila’s Blog”,
“visitors”: 6,
“page_views”: 14
}`), &struct {
*BlogPost
*Analytics
}{&post, &analytics})

临时改名struct的字段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
type CacheItem struct {
Key string `json:”key”`
MaxAge int `json:”cacheAge”`
Value Value `json:”cacheValue”`
}
json.Marshal(struct{
*CacheItem
// Omit bad keys
OmitMaxAge omit `json:”cacheAge,omitempty”`
OmitValue omit `json:”cacheValue,omitempty”`
// Add nice keys
MaxAge int `json:”max_age”`
Value *Value `json:”value”`
}{
CacheItem: item,
// Set the int by value:
MaxAge: item.MaxAge,
// Set the nested struct by reference, avoid making a copy:
Value: &item.Value,
})

用字符串传递数字

1
2
3
type TestObject struct {
Field1 int `json:”,string”`
}

这个对应的json是 {"Field1": "100"}

如果json是 {"Field1": 100} 则会报错

容忍字符串和数字互转

如果你使用的是jsoniter,可以启动模糊模式来支持 PHP 传递过来的 JSON。

1
2
3
import “github.com/json-iterator/go/extra”
extra.RegisterFuzzyDecoders()

这样就可以处理字符串和数字类型不对的问题了。比如

1
2
var val string
jsoniter.UnmarshalFromString(`100`, &val)

又比如

1
2
var val float32
jsoniter.UnmarshalFromString(`”1.23″`, &val)

容忍空数组作为对象

PHP另外一个令人崩溃的地方是,如果 PHP array是空的时候,序列化出来是[]。但是不为空的时候,序列化出来的是{"key":"value"}。 我们需要把 [] 当成 {} 处理。

如果你使用的是jsoniter,可以启动模糊模式来支持 PHP 传递过来的 JSON。

1
2
3
import “github.com/json-iterator/go/extra”
extra.RegisterFuzzyDecoders()

这样就可以支持了

1
2
var val map[string]interface{}
jsoniter.UnmarshalFromString(`[]`, &val)

使用 MarshalJSON支持time.Time

golang 默认会把 time.Time 用字符串方式序列化。如果我们想用其他方式表示 time.Time,需要自定义类型并定义 MarshalJSON

1
2
3
4
5
6
type timeImplementedMarshaler time.Time
func (obj timeImplementedMarshaler) MarshalJSON() ([]byte, error) {
seconds := time.Time(obj).Unix()
return []byte(strconv.FormatInt(seconds, 10)), nil
}

序列化的时候会调用 MarshalJSON

1
2
3
4
5
6
7
8
9
type TestObject struct {
Field timeImplementedMarshaler
}
should := require.New(t)
val := timeImplementedMarshaler(time.Unix(123, 0))
obj := TestObject{val}
bytes, err := jsoniter.Marshal(obj)
should.Nil(err)
should.Equal(`{“Field”:123}`, string(bytes))

使用 RegisterTypeEncoder支持time.Time

jsoniter 能够对不是你定义的type自定义JSON编解码方式。比如对于 time.Time 可以用 epoch int64 来序列化

1
2
3
4
5
import “github.com/json-iterator/go/extra”
extra.RegisterTimeAsInt64Codec(time.Microsecond)
output, err := jsoniter.Marshal(time.Unix(1, 1002))
should.Equal(“1000001”, string(output))

如果要自定义的话,参见 RegisterTimeAsInt64Codec 的实现代码

使用 MarshalText支持非字符串作为key的map

虽然 JSON 标准里只支持 string 作为 key 的 map。但是 golang 通过 MarshalText() 接口,使得其他类型也可以作为 map 的 key。例如

1
2
3
4
f, _, _ := big.ParseFloat(“1”, 10, 64, big.ToZero)
val := map[*big.Float]string{f: “2”}
str, err := MarshalToString(val)
should.Equal(`{“1″:”2”}`, str)

其中 big.Float 就实现了 MarshalText()

使用 json.RawMessage

如果部分json文档没有标准格式,我们可以把原始的信息用[]byte保存下来。

1
2
3
4
5
6
7
type TestObject struct {
Field1 string
Field2 json.RawMessage
}
var data TestObject
json.Unmarshal([]byte(`{“field1”: “hello”, “field2”: [1,2,3]}`), &data)
should.Equal(` [1,2,3]`, string(data.Field2))

使用 json.Number

默认情况下,如果是 interface{} 对应数字的情况会是 float64 类型的。如果输入的数字比较大,这个表示会有损精度。所以可以 UseNumber() 启用 json.Number 来用字符串表示数字。

1
2
3
4
5
decoder1 := json.NewDecoder(bytes.NewBufferString(`123`))
decoder1.UseNumber()
var obj1 interface{}
decoder1.Decode(&obj1)
should.Equal(json.Number(“123”), obj1)

jsoniter 支持标准库的这个用法。同时,扩展了行为使得 Unmarshal 也可以支持 UseNumber 了。

1
2
3
4
json := Config{UseNumber:true}.Froze()
var obj interface{}
json.UnmarshalFromString(“123”, &obj)
should.Equal(json.Number(“123”), obj)

统一更改字段的命名风格

经常 JSON 里的字段名 Go 里的字段名是不一样的。我们可以用 field tag 来修改。

1
2
3
4
5
6
7
8
output, err := jsoniter.Marshal(struct {
UserName string `json:”user_name”`
FirstLanguage string `json:”first_language”`
}{
UserName: “taowen”,
FirstLanguage: “Chinese”,
})
should.Equal(`{“user_name”:”taowen”,”first_language”:”Chinese”}`, string(output))

但是一个个字段来设置,太麻烦了。如果使用 jsoniter,我们可以统一设置命名风格。

1
2
3
4
5
6
7
8
9
10
11
12
import “github.com/json-iterator/go/extra”
extra.SetNamingStrategy(LowerCaseWithUnderscores)
output, err := jsoniter.Marshal(struct {
UserName string
FirstLanguage string
}{
UserName: “taowen”,
FirstLanguage: “Chinese”,
})
should.Nil(err)
should.Equal(`{“user_name”:”taowen”,”first_language”:”Chinese”}`, string(output))

使用私有的字段

Go 的标准库只支持 public 的 field。jsoniter 额外支持了 private 的 field。需要使用 SupportPrivateFields() 来开启开关。

1
2
3
4
5
6
7
8
9
import “github.com/json-iterator/go/extra”
extra.SupportPrivateFields()
type TestObject struct {
field1 string
}
obj := TestObject{}
jsoniter.UnmarshalFromString(`{“field1″:”Hello”}`, &obj)
should.Equal(“Hello”, obj.field1)

下面是我补充的内容

忽略掉一些字段

原文中第一节有个错误,我更正过来了。omitempty不会忽略某个字段,而是忽略空的字段,当字段的值为空值的时候,它不会出现在JSON数据中。

如果想忽略某个字段,需要使用 json:"-"格式。

1
2
3
4
5
type User struct {
Email string `json:”email”`
Password string `json:”password”`
// many more fields…
}

如果想临时忽略掉空Password字段,可以用-:

1
2
3
4
5
6
json.Marshal(struct {
*User
Password bool `json:”-“`
}{
User: user,
})

忽略掉一些字段2

如果不想更改原struct,还可以使用下面的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
type User struct {
Email string `json:”email”`
Password string `json:”password”`
// many more fields…
}
type omit *struct{}
type PublicUser struct {
*User
Password omit `json:”-“`
}
json.Marshal(PublicUser{
User: user,
})