微服务之服务监控和治理、容错隔离与 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/

GO语言调试利器dlv快速上手

https://www.cnblogs.com/realjimmy/p/13418508.html

一、dlv的安装

1)下载dlv

git clone https://github.com/go-delve/delve.git $GOPATH/src/github.com/go-delve/delve

或者 go get github.com/derekparker/delve/cmd/dlv

2)安装

cd $GOPATH/src/github.com/go-delve/delve

make install

二、dlv简要使用说明

2.1、获取帮助信息

安装后执行dlv -h将会看到帮助信息:

image

上面的信息只是列出了命令列表,具体使用方法没有给出,我们可以执行dlv help + 具体命令来查看详细说明,

比如我们执行dlv help attach:

image

2.2、进入调试模式

1)dlv attach pid:类似与gdb attach pid,可以对正在运行的进程直接进行调试(pid为进程号)。

2)dlv debug:运行dlv debug test.go会先编译go源文件,同时执行attach命令进入调试模式,该命令会在当前目录下生成一个名为debug的可执行二进制文件,退出调试模式会自动被删除。

3)dlv exec executable_file :直接从二进制文件启动调试模式。如果要带参数执行需要添加–,如dlv exec executable_file — -f xxx.conf

4)dlv core executable_file core_file:以core文件启动调试,通常进行dlv的目的就是为了找出可执行文件core的原因,通过core文件可直接找出具体进程异常的信息。

3、常用调试方法

3.1 dlv trace追踪调用轨迹

该命令最直接的用途是可以追踪代码里函数的调用轨迹,

如下源代码,现用trace命令跟踪其调轨迹。

package main import ( “fmt” “time” ) func Test() { fmt.Println(“hello”) time.Sleep(1000 * 1000 * 100) } func Test2() { fmt.Println(“world”) time.Sleep(1000 * 1000 * 100) } func main() { for i := 0; i < 2; i++ { go Test() go Test2() } time.Sleep(1000 * 1000 * 2000) fmt.Println(“end”) }

运行结果,这里看除了Test,test2也被追踪:

$ dlv trace hello.go Test

> goroutine(19): main.Test2()

> goroutine(21): main.Test2()

> goroutine(18): main.Test()

world

hello

world

> goroutine(20): main.Test()

hello

=> ()

=> ()

=> ()

=> ()

end

3.2 调试模式基本命令

这里用上节的源码作为示例进行调试。开始调试:dlv debug hello.go

1)b(break):打断点

设置断点,当需要设置多个断点时,为了断点可识别可进行自定义命名。进入调试模式后先打断点。

例:b Test

b test.go:13

image

2)r(restart):重启当前进程

类似gdb里的run,如果刚执行dlv debug hello.go,进程已经起来,不用执行。如果进程已结算或需要重新开始则需要执行r

3)c(continue):继续执行到断点处

image

4)bp:查看所有断点

image

5)on  :当运行到某断点时执行相应命令

断点可以是名称(在设置断点时可命名断点)或者编号,例如on 3 p i表示运行到断点3时打印变量i。

image

6)cond(condition)   :有条件的断点

针对某个断点,只有表达式成立才会被中断。例:

condition 3 i==1

image

image

7)n(next):逐行执行代码,不进入函数内

8)s(step):逐行执行代码,遇到函数会跳进内部

9)stepout:当使用s命令进入某个函数后,执行它可跳出函数

10)si(step-instruction):单步单核执行代码

如果不希望多协程并发执行可以使用该命令,这在多协程调试时极为方便。

11)args:查看被调用函数所传入的参数值

12)locals:查看所有局部变量

locals var_name:查看具体某个变量,var_name可以是正则表达式。

13)clear:清除单个断点

14)clearall:清除所有断点

15)list:打印当前断点位置的源代码

list后面加行号可以展示该行附近的源代码,要注意该行必须是代码行而不能是空行。

16)bt:打印当前栈信息。

3.3 多协程调试

1)goroutines:显示所有协程

image

2)goroutine:协程切换

先执行goroutine 7表示切换到7号协程上

3.4 其他命令

1)frame:切换栈。

2)regs:打印寄存器内容。

3)sources:打印所有源代码文件路径

4)source:执行一个含有dlv命令的文件

source命令允许将dlv命令放在一个文件中,然后逐行执行文件内的命令。

5)trace:类似于打断点,但不会中断,同时会输出一行提示信息

go 静态检查工具

看了看日历,现在已经是 2021 年了,偶尔还是能看到有人在发诸如 《http body 未关闭导致线上事故》,或者 《sql.Rows 未关闭半夜惊魂》类的文章,令人有一种梦回 2015 的感觉。

在这个 Go 的静态分析工具已经强到烂大街的时代,写这些文章除了暴露这些人所在的公司基础设施比较差,代码质量低以外,并不能体现出什么其它的意思了。毕竟哪怕是不懂怎么读源码,这样的问题你 Google 搜一下也知道是怎么回事了。

特别是有些人还挂着大公司的 title,让人更加不能理解了。下面是简单的静态分析工具的科普,希望给那些还在水深火热的 Gopher 们送点解药。

何谓静态分析

静态分析是通过扫描并解析用户代码,寻找代码中的潜在 bug 的一种手段。

静态分析一般会集成在项目上线的 CI 流程中,如果分析过程找到了 bug,会直接阻断上线,避免有问题的代码被部署到线上系统。从而在部署早期发现并修正潜在的问题。

图片

社区常见 linter

时至今日,社区已经有了丰富的 linter 资源供我们使用,本文会挑出一些常见 linter 进行说明。

go lint

go lint 是官方出的 linter,是 Go 语言最早期的 linter 了,其可以检查:

  • 导出函数是否有注释
  • 变量、函数、包命名不符合 Go 规范,有下划线
  • receiver 命名是否不符合规范

但这几年社区的 linter 蓬勃发展,所以这个项目也被官方 deprecated 掉了。其主要功能被另外一个 linter:revive[^1] 完全继承了。

go vet

go vet 也是官方提供的静态分析工具,其内置了锁拷贝检查、循环变量捕获问题、printf 参数不匹配等工具。

比如新手老手都很容易犯的 loop capture 错误:

package main

func main() {
 var a = map[int]int {1 : 1, 2: 3}
 var b = map[int]*int{}
 for k, r := range a {
  go func() {
   b[k] = &r
  }()
 }
}

go vet 会直接把你骂醒:

~/test git:master ❯❯❯ go vet ./clo.go
# command-line-arguments
./clo.go:8:6: loop variable k captured by func literal
./clo.go:8:12: loop variable r captured by func literal

执行 go tool vet help 可以看到 go vet 已经内置的一些 linter。

~ ❯❯❯ go tool vet help
vet is a tool for static analysis of Go programs.

vet examines Go source code and reports suspicious constructs,
such as Printf calls whose arguments do not align with the format
string. It uses heuristics that do not guarantee all reports are
genuine problems, but it can find errors not caught by the compilers.

Registered analyzers:

    asmdecl      report mismatches between assembly files and Go declarations
    assign       check for useless assignments
    atomic       check for common mistakes using the sync/atomic package
    bools        check for common mistakes involving boolean operators
    buildtag     check that +build tags are well-formed and correctly located
    cgocall      detect some violations of the cgo pointer passing rules
    composites   check for unkeyed composite literals
    copylocks    check for locks erroneously passed by value
    errorsas     report passing non-pointer or non-error values to errors.As
    httpresponse check for mistakes using HTTP responses
    loopclosure  check references to loop variables from within nested functions
    lostcancel   check cancel func returned by context.WithCancel is called
    nilfunc      check for useless comparisons between functions and nil
    printf       check consistency of Printf format strings and arguments
    shift        check for shifts that equal or exceed the width of the integer
    stdmethods   check signature of methods of well-known interfaces
    structtag    check that struct field tags conform to reflect.StructTag.Get
    tests        check for common mistaken usages of tests and examples
    unmarshal    report passing non-pointer or non-interface values to unmarshal
    unreachable  check for unreachable code
    unsafeptr    check for invalid conversions of uintptr to unsafe.Pointer
    unusedresult check for unused results of calls to some functions

默认情况下这些 linter 都是会跑的,当前很多 IDE 在代码修改时会自动执行 go vet,所以我们在写代码的时候一般就能发现这些错了。

但 go vet 还是应该集成到线上流程中,因为有些程序员的下限实在太低。

errcheck

Go 语言中的大多数函数返回字段中都是有 error 的:

func sayhello(wr http.ResponseWriter, r *http.Request) {
 io.WriteString(wr, "hello")
}

func main() {
 http.HandleFunc("/", sayhello)
 http.ListenAndServe(":1314", nil) // 这里返回的 err 没有处理
}

这个例子中,我们没有处理 http.ListenAndServe 函数返回的 error 信息,这会导致我们的程序在启动时发生静默失败。

程序员往往会基于过往经验,对当前的场景产生过度自信,从而忽略掉一些常见函数的返回错误,这样的编程习惯经常为我们带来意外的线上事故。例如,规矩的写法是下面这样的:

data, err := getDataFromRPC()
if err != nil {
 return nil, err
}

// do business logic
age := data.age

而自信的程序员可能会写成这样:

data, _ := getDataFromRPC()

// do business logic
age := data.age

如果底层 RPC 逻辑出错,上层的 data 是个空指针也是很正常的,如果底层函数返回的 err 非空时,我们不应该对其它字段做任何的假设。这里 data 完全有可能是个空指针,造成用户程序 panic。

errcheck 会强制我们在代码中检查并处理 err。

gocyclo

gocyclo 主要用来检查函数的圈复杂度。圈复杂度可以参考下面的定义:

圈复杂度(Cyclomatic complexity)是一种代码复杂度的衡量标准,在 1976 年由 Thomas J. McCabe, Sr. 提出。在软件测试的概念里,圈复杂度用来衡量一个模块判定结构的复杂程度,数量上表现为线性无关的路径条数,即合理的预防错误所需测试的最少路径条数。圈复杂度大说明程序代码可能质量低且难于测试和维护,根据经验,程序的可能错误和高的圈复杂度有着很大关系。

看定义较为复杂但计算还是比较简单的,我们可以认为:

  • 一个 if,圈复杂度 + 1
  • 一个 switch 的 case,圈复杂度 + 1
  • 一个 for 循环,圈复杂度 + 1
  • 一个 && 或 ||,圈复杂度 + 1

在大多数语言中,若函数的圈复杂度超过了 10,那么我们就认为该函数较为复杂,需要做拆解或重构。部分场景可以使用表驱动的方式进行重构。

由于在 Go 语言中,我们使用 if err != nil 来处理错误,所以在一个函数中出现多个 if err != nil 是比较正常的,因此 Go 中函数复杂度的阈值可以稍微调高一些,15 是较为合适的值。

下面是在个人项目 elasticsql 中执行 gocyclo 的结果,输出 top 10 复杂的函数:

~/g/s/g/c/elasticsql git:master ❯❯❯ gocyclo -top 10  ./
23 elasticsql handleSelectWhere select_handler.go:289:1
16 elasticsql handleSelectWhereComparisonExpr select_handler.go:220:1
16 elasticsql handleSelect select_handler.go:11:1
9 elasticsql handleGroupByFuncExprDateHisto select_agg_handler.go:82:1
9 elasticsql handleGroupByFuncExprDateRange select_agg_handler.go:154:1
8 elasticsql buildComparisonExprRightStr select_handler.go:188:1
7 elasticsql TestSupported select_test.go:80:1
7 elasticsql Convert main.go:28:1
7 elasticsql handleGroupByFuncExpr select_agg_handler.go:215:1
6 elasticsql handleSelectWhereOrExpr select_handler.go:157:1

bodyclose

使用 bodyclose[^2] 可以帮我们检查在使用 HTTP 标准库时忘记关闭 http body 导致连接一直被占用的问题。

resp, err := http.Get("http://example.com/") // Wrong case
if err != nil {
 // handle error
}
body, err := ioutil.ReadAll(resp.Body)

像上面这样的例子是不对的,使用标准库很容易犯这样的错。bodyclose 可以直接检查出这个问题:

# command-line-arguments
./httpclient.go:10:23: response body must be closed

所以必须要把 Body 关闭:

resp, err := http.Get("http://example.com/")
if err != nil {
 // handle error
}
defer resp.Body.Close() // OK
body, err := ioutil.ReadAll(resp.Body)

HTTP 标准库的 API 设计的不太好,这个问题更好的避免方法是公司内部将 HTTP client 封装为 SDK,防止用户写出这样不 Close HTTP body 的代码。

sqlrows

与 HTTP 库设计类似,我们在面向数据库编程时,也会碰到 sql.Rows 忘记关闭的问题,导致连接大量被占用。sqlrows[^3] 这个 linter 能帮我们避免这个问题,先来看看错误的写法:

rows, err := db.QueryContext(ctx, "SELECT * FROM users")
if err != nil {
    return nil, err
}

for rows.Next() {
 err = rows.Scan(...)
 if err != nil {
  return nil, err // NG: this return will not release a connection.
 }
}

正确的写法需要在使用完后关闭 sql.Rows:

rows, err := db.QueryContext(ctx, "SELECT * FROM users")
defer rows.Close() // NG: using rows before checking for errors
if err != nil {
    return nil, err
}

与 HTTP 同理,公司内也应该将 DB 查询封装为合理的 SDK,不要让业务使用标准库中的 API,避免上述错误发生。

funlen

funlen[^4] 和 gocyclo 类似,但是这两个 linter 对代码复杂度的视角不太相同,gocyclo 更多关注函数中的逻辑分支,而 funlen 则重点关注函数的长度。默认函数超过 60 行和 40 条语句时,该 linter 即会报警。

linter 集成工具

一个一个去社区里找 linter 来拼搭效率太低,当前社区里已经有了较好的集成工具,早期是 gometalinter,后来性能更好,功能更全的 golangci-lint 逐渐取而代之。目前 golangci-lint 是 Go 社区的绝对主流 linter。

golangci-lint

golangci-lint[^5] 能够通过配置来 enable 很多 linter,基本主流的都包含在内了。

在本节开头讲到的所有 linter 都可以在 golangci-lint 中进行配置,

使用也较为简单,只要在项目目录执行 golangci-lint run . 即可。

~/g/s/g/c/elasticsql git:master ❯❯❯ golangci-lint run .
main.go:36:9: S1034: assigning the result of this type assertion to a variable (switch stmt := stmt.(type)) could eliminate type assertions in switch cases (gosimple)
 switch stmt.(type) {
        ^
main.go:38:34: S1034(related information): could eliminate this type assertion (gosimple)
  dsl, table, err = handleSelect(stmt.(*sqlparser.Select))
                                 ^
main.go:40:23: S1034(related information): could eliminate this type assertion (gosimple)
  return handleUpdate(stmt.(*sqlparser.Update))
                      ^
main.go:42:23: S1034(related information): could eliminate this type assertion (gosimple)
  return handleInsert(stmt.(*sqlparser.Insert))
                      ^
select_handler.go:192:9: S1034: assigning the result of this type assertion to a variable (switch expr := expr.(type)) could eliminate type assertions in switch cases (gosimple)
 switch expr.(type) {

参考资料

[1] https://revive.run/

[2] https://github.com/timakin/bodyclose

[3] https://github.com/gostaticanalysis/sqlrows

[4] https://github.com/ultraware/funlen

[5] https://github.com/golangci/golangci-lint

[转]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 文档。

对simhash的简单理解

-1为什么这么长

我整理这个文档的最初目的是将simhash的基本原理搞清楚,这个初心在学习的过程中逐渐改变了。

在整理和学习simhash相关资料的过程中,我不理解simhash得到的文本特征有效性的来源,于是开始了解simhash所属的局部敏感哈希,尝试找到解释。从论文和大家的博客来看,随机投影hash和随机超平面hash局部敏感哈希的经典形式。于是,我盯着这两个“随机”朋友看了很久。当了解到随机投影是一种数据降维方法的时候,我终于相信这两个hash算法区别不是特别大。

为了记录simhash相关知识内容,也为了记录自己在知识地图中搜索的过程,我搞了一个新花样——大杂烩。新花样总的来说会让人的大脑分泌一些带来快乐的东西,然后露出如图-1-1的表情。这也是强化自己的学习动力的一种方式。


图-1 -1当手下又有鬼点子时

本文的目录如图-1-2。


图-1-2 目录

0引言

21世纪不是生物的世纪,而是数据的世纪。数据已经成为我们这个文明必不可少的一部分,不管走到哪里,我们都沐浴在数据形成的空气中。现在,这种空气有点太多,大家快要氧中毒了——互联网、工业生产等领域产生的数据太多,我们在管理和使用这些数据时遇到了几种困难:高维度、极大的数量、重复问题。我们需要使用包括simhash在内的算法来解决这些问题。

0.1我要降维

随着采集能力和存储能力的强大,我们得了贪食症,不管一个字段是否有用,先存起来再说。总的来说,字段越多,我们可以构造的特征就越多、可以输入模型的信息就越多。不过呢,特征多是有代价的,它会降低模型的训练和计算速度。但是,我比较贪心,就是要马儿不吃草、还跑得快:在特征数量较小的情况下,尽量多地向模型提供信息。

那只能降维了。

0.2我要搜索

采集和存储能力的强大,也让我们的数据积累了海量的文档、“啥都有”。但是,如果没有较强的检索能力,“海量数据”就是“啥都找不到”“啥都没有”的代名词。

我们需要强有力的搜索技术。

0.3我需要去重

很多情况下,我们需要对文本数据进行去重:当文本分类语料有重复的样本;有些网站会将同一片文章重复发若干次,导致我们采集到大量重复的内容,进而影响后续热点检测等任务的效果;等等。

假如有10篇文档需要去重,我可以亲自上、手工排重。这个任务几十秒钟就完成了。

假如有10000篇文档需要去重,可以使用适当的相似度算法判断是否重复,然后用single-pass的框架对文档进行聚类 ,最后保留所有簇的中心文档即可。这种算法最多需要计算大约(1+9999)*10000/2次相似度,耗时会很久。当然了,我们可以用一个倒排索引来加速。

假如我们有100000000篇文档需要去重,按我以前的套路,就是用Spark这样的工具咔咔算,算到天荒地老。不过呢,如果我们使用合适的文本相似度算法和倒排索引的构建方法,用一台内存足够的机器,可以在有生之年完成这个任务。

我需要一个高效的文本去重算法。

0.4我要局部敏感哈希

前面所说的几个需求,我们可以用很多方法来满足。可选的方案中,有一朵奇葩,那就是局部敏感哈希(Local Sensitive Hash)。LSH的思想是,在保留数据相对位置的条件下,将原始数据映射到一个碰撞率较高的低维的新空间里,从而降低下游任务的计算量。利用局部敏感哈希编码的高碰撞率,我们可以设计出更加稀疏的倒查索引键值,从而得到非常高效的文本去重方案,即基于simhash的文本去重方法。

如图0-1,是我在整理simhash相关的内容时,探索出来的知识地图,算是本文的另一种目录。


图0-1 已探明的知识地图

1从基于随机投影的数据降维说起

1.1向量点积的含义

随机投影的基础方法,是向量点积运算。而理解随机投影的基础,是理解向量点积运算的含义。如图1-1,二维平面中有两个向量

。二者的点积为:

一般来说,我们会把向量点积理解为,计算向量夹角时的中间结果。

这里,为了帮助理解随机投影,需要重新解释一下。向量点积表示的是,向量

在向量

法平面上投影长度的加权值,其中权重为

的长度(当然由于乘法可交换,这里两个向量可以互换)。得到的投影向量为:

如果我们将

看做一个新空间的坐标轴,那么,在新的空间中,点A的坐标就是

这样,我们就用向量

完成了对点A的映射。

举个例子,假设两个向量:

那么a.b=7*1+3*(-1)=4。点A以

向量所在直线为坐标轴的空间中,坐标是4。

注意,经过这通操作,点A(在新空间中)的坐标由2维降到了1维。


图1-1 向量点积的含义

1.2基于随机投影的降维——2维到1维

Johnson–Lindenstrauss引理指出,在欧式空间中的若干点,经过相同的映射后进入新的空间,它们仍然会保持原来的相对位置。简单来说,“相对位置”指的是一些点相距较近,一些点相距较远。这里只从直觉上展示随机投影“保留数据相对位置”的性质。具体的证明暂时就免了。

假设二维平面中,有若干点A=(7,3), C=(-0.5,-1.5),D=(9,2),E=(-0.5,2.5)。目测,A和D离得比较近;C和E里的比较近。

我们使用向量

,将这4个点映射到新的空间,坐标分别是A1=4,C1=1,D1=7,E1=-3。A1和D1里的比较近;C1和E1离得比较近;当然了,C1和D1也挺近的。这样,二维坐标系里的4个点,被映射到了一个1维空间中,还在一定程度上保留了原来的相对位置。

那么,

是怎么来的呢?随机生成。我们可以基于高斯分布,生成B点的横坐标和纵坐标。为啥是高斯分布呢?有一定的原因,这里暂时无力讲述

这可省事了,我们在原始数据所属的空间中,随机生成一个向量,就可以基于这个向量,将原始数据映射到一个1维空间中。不过呢,在新空间中,数据点之间的相对距离存在一定程度的失真,怎么办呢?


图1-2 二维平面里的几个点

1.3随机投影的降维——N维到K维

假设有M条数据,维度非常高,为N。随机投影法将帮我们把这种数据压缩为K维(K远小于N)。


图1-1 二维平面上的原始数据向量和随机向量

将原始数据看做高维空间中的一个点。那么对应第m条数据,有一个向量

,从原点出发到这个数据点。

我们随机生成K(K远小于N)个长度为N的一维向量

,将原始数据映射到以这些随机向量所在直线为坐标轴的新空间中,得到点

。这种降维方法叫做“随机投影法”。总的来说,K越大,降维后的数据,保留的信息越多,数据之间的相对位置越“保真”——可以证明,但是这里无力搞了。

1.4随机投影法降维的python实现

github地址:

https://github.com/lipengyuer/DataScience/blob/master/src/algoritm/RandomProjectionDimReduction.py​github.com

2随机投影与局部敏感哈希

2.1随机投影降维的简化——基于随机投影的局部敏感哈希

在1.3中提到,随机投影法可以把高维数据映射到新的空间中。由于特征维度大大降低了,聚类、分类、搜索等任务的计算量也大大降低了。不过呢,人心不足蛇吞象,我还想进一步降低计算量。

新的空间里,坐标轴还是实数轴,降维后的特征是连续特征。有人认为,这样的话还得用float这种类型存储数据、还得进行float*float的操作,浪费。有些高手本着如图2-1的原则,对随机投影降维进行了改进。


图2-1 工程师的追求

在将数据映射到新空间后,我们将落在坐标轴负轴的维度(该维度取值为负数),统一赋值为0,表示数据与对应随机向量夹角大于90度。或者说,数据点与随机向量,在以后者为法向量、过原点的超平面的同侧。类似的,我们将落在坐标轴非负轴的维度,统一赋值为1。这样原始数据就被映射到了一个离散的新空间里。

这里所述的数据映射方法,就是我们常说的基于随机投影的局部敏感哈希。局部敏感哈希,与常见的hash有啥关系呢?局部敏感哈希是hash算法的一种,是数据映射方法,通常被用来对数据进行降维。

2.2黑白分明的hash

一般来说,我们遇到的数据长度n是可变的。我们可以用hash函数,将数据映射为一个长度固定为K(K远小于n)的编码。Hash函数的输出,是对原始数据的一种压缩表示,也叫做数字签名、数字指纹、消息摘要或者散列值。常见的hash算法非常严格,要求碰撞率尽量低,即每条数据拥有一个唯一的id。

从hash的原理来看,内容相同的文档,具有相同的哈希码;内容略有不同的文档,对应的哈希码会有很大的不同。如表2-1,是一些句子的hash值。其中句子1和3的哈希码相等;句子1和句子4只差了一个字,哈希码具有肉眼可见的巨大差异。

Hash码可以用来判断两个字符串是否相等。这样做有什么意义呢?我们把文档的哈希码存储起来,当需要判断两篇文档是否相等时,直接判断二者的哈希码即可。如果文档比较长(几百字),而哈希码比较短(几十位),那么后者的相等判断是比较快的。如果说,我们用位运算来进行哈希码的相等判断,那就更快了。因此,基于哈希码来判断文档内容是否相同,在一些场景里是非常高效的方案。

表2-1 句子们和它们的hash编码

在信息检索任务中,我们可以为每一条记录生成一个hash码、作为id,这样就可以为一篇文档快速地找到相关的字段了。然而,假如需要检索的是相似的文档,普通的hash算法就没办法了。这时候,我们需要不太严格的hash编码方法。

2.3随机投影hash的用途

局部敏感哈希是非常适合文本数据检索场景的编码方法。 局部敏感哈希在随机投影降维方法的基础上,增加了离散化环节。这个“离散”有啥用呢?

直观地看,原来空间中的-0.66和-100,在新空间都成为1或者0,也就是说,二者在新的空间中是相等的。这样的结果是,在原来空间中比较接近的数据点,在新的空间中,在一些维度上可能是相等的。

基于取值相同的维度,我们可以把数据分组(俗称分到桶里)。在搜索文档的时候,我们对查询语句进行hash,然后把各个维度上、桶的编号相同的文档召回,最后进行精排,可以实现快速而高质量的搜索。一个基于分桶构建的倒查索引可以帮助我们实现这样的操作。

一些在原来空间接近的数据点,经过这样的映射后,仍然相似甚至相等(各个维度坐标都相等)。对二进制编码进行相似度计算是非常快速的,在这个基础上我们可以进行非常快速的聚类。

另外,我们可以把映射后相等的数据看做是相同的数据,进而进行排重。

2.4随机超平面hash

随机超平面hash是在随机投影hash的基础上发展而来的。二者的名称含义相近,就像算法内容一样。与随机投影hash的主要区别是,随机超平面hash的编码结果是-1和1组成的串——与随机向量点积为负数,新空间中该维度取值-1;与随机向量点积为非负数,新空间中该维度取值为1。

由于使用了新空间中所有象限,随机超平面hash的性能更好一些,在文本分类、聚类任务中表现更好。

2.5两种局部敏感哈希的python实现

GitHub地址:

https://github.com/lipengyuer/DataScience/blob/master/src/nlp/LSH/RandomProjectionLSH.py​github.com

https://github.com/lipengyuer/DataScience/blob/master/src/nlp/LSH/RandomHyperplaneLSH.py​github.com

3局部敏感哈希的杰出代表:simhash

“hash”是我们经常使用的一种对数据的映射操作,它会把特定类型的数据(如字符串)映射为另一种数据(比如内存地址)。“simhash”是一种将文本数据映射为固定长度的二进制编码的算法。当然了,由于基于simhash的文本相似度计算方法、文本去重方法名气比较大,大家经常以“simhash”代指“基于simhash编码的文本相似度计算方法”或者“基于simhash编码表示的文本去重方法”。这样混乱的称呼,特别容易引起混淆,因此这里约定几个提法:(1)simhash是一种将文本数据映射为定长二进制编码的hash算法,用NLP的说法就是一种文本表示模型 ;(2)基于simhash的文本相似度计算,就是用文本的simhash值作为特征向量,来计算文档之间的相似度或者距离;(3)基于simhash的文本去重,就是使用基于simhash表示来计算文本相似度,进而发现重复文档并删除。

3.1历史选择了simhash

在文本分类、聚类、相似度计算等等任务中,我们希望用一个定长的数值向量表示文本。这样我们才能用欧氏距离等方法计算文本的相似度。常见的文本表示模型有TF(Term Frequency)、TF-IDF(Term Frequency-Inverse Document Frequency)、句向量等等。

在海量文本去重场景下,上述几种模型都有一些不足:

(1)TF精度较差,即对“非常相似”到“相同”这个相似度范围的判断不是很敏感;受文本长度等因素影响较大,相似度阈值定起来比较麻烦。

(2)精度也比较差; TF-IDF要求首先遍历一遍所有文本来的得到IDF,计算量比较大;

(3)句向量主要关注的是语义,对字面的相似不太擅长;计算量比较大。

而simhash克服了以上几种算法的不足。

3.2从文本的词向量空间模型到simhash

在词向量空间模型中,我们把词语映射为一个独热编码(one-hot encoding)。假设词汇表为(我,是,中国人,农民,儿子),大小为5,那么这几个词语的独热编码如表3-1。将文本分词后,把词语的独热编码按位累加,就得到了TF向量。比如“我是一个码农,是农民的儿子”,分词后得到“我/是/一个/码农/,/是/农民/的/儿子”,基于词语编码表,得到的TF向量就是(1,2,0,1,1)。

表3-1 词语的独热编码

高维度是TF的主要缺点。一般来说,我们的词汇是数以万计的,TF向量会比较长,导致下游任务计算量比较大。比如计算两篇文本的欧氏距离,我们需要执行数万次的减法、平方、加法等。

为此,Simhash从词语编码的降维入手,实现了对TF的降维。

3.3simhash的计算过程

Simhash是如何将文本映射为数值向量的呢?

首先,它基于hash算法将词语映射为较短的编码,然后使用随机超平面hash的做法得到最终的词语编码。然后,就像TF的计算一样,将每个词语的编码加起来。最后,采用随机超平面的离散化方法,得到文本的最终表示。

假设我们需要把S=“我爱北京天安门”这句话转换成长度为5的二进制编码。

3.3.1分词

按照一定的粒度切分文本,比如分词。S可以切分为”我/爱/北京/天安门”.

3.3.2将词语映射为定长二进制编码

将文本的每一个碎片,用一种hash算法映射为一定长度的二进制编码。如表3-2,是我假想的几个词语的编码值。

表3-2 S的词语的hash值

3.3.3将词语的二进制编码再转换一下

我们需要把词语二进制编码中的“0”一律转换为“-1”,得到词语的最终编码,如表3-3。将0转换为-1的目的,是将映射后的词语放置在整个空间中,而不是某一个象限,这样可以让数据点分布得更均匀一点。

注意,TF里,每个词语的权重比较粗暴,如果想升级一下,可以考虑TF-IDF。

与随机超平面hash相比,这里使用了一个“不随机”的超平面,将空间进行了分割。

表3-3 S的词语的新编码

3.3.4计算文档的初级编码

我们将句子中所有词语的编码加起来,就得到了句子的编码(0,0,0,2,2)。

3.3.5计算文档的最终编码

将句子编码的非正数元素转换为0,正数元素转换为1,就得到了句子的最终编码(0,0,0,1,1)——这就是句子的simhash编码。

3.4基于simhash编码计算文本距离

Simhash完成了文本的低纬度表示 ,接下来我们就可以进行文本相似度、文本分类等任务了。这里以文本相似度计算为例,展示simhash码的使用方式。

3.4.1海明距离

海明距离是simhash的经典搭档,用来在simhash码的基础上度量文本之间的距离或相似度。假设有两个等长(长度为K)的数据串:

二者的海明距离等于:

其中

假设文档A和文档B的simhash码分别为(1,1,0,1)和(1,0,0,1),那么二者的海明距离就是0+1+0+0=1(感谢

victor diao​www.zhihu.com图标

的提醒,之前这里是”0+1+1+0=2″,是一个错误)。

海明距离的特点是,在实际应用中,只需要进行基本数据类型的相等判断和加法,计算速度非常快。

3.4.2用位运算对simhash升级

Simhash码是二进制编码,可以使用位运算来实现一些快速的操作。这是simhash高性能的一个重要来源。

3.5基于simhash计算文本距离的python实现

github地址是:

https://github.com/lipengyuer/DataScience/blob/master/src/nlp/LSH/simhash_v1.py​github.com

https://github.com/lipengyuer/DataScience/blob/master/src/nlp/LSH/simhash_v2.py​github.com

4基于simhash的文本去重框架

基于simhash的海量文本去重框架里同时涉及了搜索和聚类的关键技术,可以快速地修改为搜索和聚类工具。因此,这里用一个基于simhash 的文本去重框架展示simhash的应用方式。

假设我们需要对M篇文档进行去重。

4.1比较暴力的去重框架

最简单的方式,是计算每两篇文档之间的距离,然后对距离不超过3的文档对进行去重处理。这样的话,我们需要计算(M-1+0)*M/2词相似度,计算量非常大。

4.2使用倒查索引优化去重框架

我们可以使用倒查索引来降低计算量。以64位simhash编码表示的数据集为例,我们可以构建64个倒查索引,对应simhash码的64个维度;每个倒查索引只有两个key,即0和1,表示文本编码在这个维度上的取值;这样,我们就可以把所有的文档,按照simhash编码在各维度上的取值,放到各个倒查索引中。

在实际去重的时候,每遍历到一个不重复的文档,就把它添加到64个倒查索引中。

在考察一篇文档是否重复的时候,我们首先把64个倒查索引中,与当前文档编码匹配的部分召回,然后比对当前文档与召回文档的相似度,进而判断是否重复。这种查询方式比4.1所述的方式,需要比对的次数要少很多。

当然,这种倒查索引的key还是比较稠密,每次查询会召回比较多的文档。

4.3基于抽屉原理升级倒查索引

我们可以设计更稀疏的key,来获得更高的查询精度,进而进一步减少召回的文档数量。

4.3.1抽屉原理

我们可以用抽屉原理来对去重框架进行升级。

如图4-1,有3个抽屉、4个苹果。将4个苹果放到抽屉里,那么至少一个抽屉里,有2个苹果。当我们有X个抽屉、X+1个苹果,可以得到同样的结论,至少有一个抽屉里有2个苹果。这就是抽屉原理。


图 4-1个苹果与3个抽屉

一般来说文本重复与否海明距离阈值是3,当两篇文档被判定相似,那么二者的simhash码最多有3个位置是不相等的(感谢

AndrewFu​www.zhihu.com图标

的提醒,原来是“相等”,现已改正)。换句话说,如果两篇文档的simhash 编码的海明距离小于等于3,我们认为二者是相似的。假设我们的simhash编码是64位,可以分为4组连续的数字。那么两篇相似文档的simhash编码中,至少有一个子串是完全相等的。换句话说,只有包含了文档A的4个子串中的一个的文档,才有可能与A相似。

4.3.2更稀疏的key

我们可以把simhash码切分为4个(数字可以变化)连续的子串,然后以子串为key取构建倒排索引。这时候,倒查索引的数量降到了4个。每个字串是一个16位的二进制编码,命中的文档数量相比4.2所述key更少。结果就是,查询的时候,召回的文档数量更少了。

4.4完整的去重框架

如图4-2和图4-3,分别是向倒查索引添加文档,和召回文本并排重的框架。


图4-2 构建倒排

图4-3 查询的过程

4.5文本去重框架的python实现

github地址是:

https://github.com/lipengyuer/DataScience/blob/master/src/nlp/LSH/NearRedupRemove.py​github.com

5结语

Simhash的背后是局部敏感哈希;基于simhash的文本去重技术背后,是基于局部敏感哈希的海量数据检索技术。

simhash也可以看做是一种文本你的分布式表示方法。难得的是,这种方法是无监督的。当然了,simhash挖掘文本信息的能力没有word2vec这种模型强,simhash编码会限制分类、聚类模型的效果上限。一般来说,在低精度、高速度的场景下,可以试一下simhash。

这里对局部敏感哈希的理解方式,具有较强的个人风格,不一定适合所有的人。如果看不懂,那是我的表达方式导致的。可以参考这里的知识地图搜索套路,整理出属于自己的表达。

 

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

然后同步数据即可!

 

scp免密码操作

在一些脚本中scp需要免密操作,这就需要机器通过rsa进行相互授权。

一,client端

1)生成密钥:ssh-keygen -t rsa

2)拷贝密钥 cat ~/.ssh/id_rsa.pub

二,server端

将刚刚拷贝的密钥直接放在~/.ssh/authorized_keys 即可,这样client端访问server就需要密码了,非常简单

解决git alias权限问题

git aliases causing “Permission denied” error

 

The correct answer to this was actually different. Before git runs the aliases it checks the $PATH. In case the directory does not exist, or lacks permissions, git produces the "fatal: cannot exec 'git-co': Permission denied". It does not ever comes to check the aliases so git foobar will produce the same error.

Good people from the git mailing list also reminded me of an strace tool, that can help finding the entry that is returning EACCES, as in: strace -f -e execve git foobar

 

简单来说就行执行 strace -f -e execve git foobar

看看哪个路径在PATH中,并且没有权限,直接去掉就好了。

echo $PATH 可以查看当前的环境变量

PATH=“xxxx”可以进行赋值

禁用代理APP抓包方法

目前在ios/android上很多app选择禁止使用代理,这就让目前主流的fiddler,charles等无法抓包,但是通过流量采集的wireshark还是可以继续抓包的,目前摘抄一个可用的方法,原文地址:

 

参考文档

www.wireshark.org

Mac OS X上使用Wireshark抓包 (抓取手机网络)
使用wireshark以及filddler配合抓去手机端的TCP以及HTTP请求
Wireshark基本介绍和学习TCP三次握手
wireshark使用教程
Mac下使用Wireshark抓iphone手机数据包
Wireshark抓包分析/TCP/Http/Https及代理IP的识别

Fiddler 教程

一、Mac抓iPhone数据包

原理:使用 Mac 抓取 iPhone 数据包可通过共享和代理两种方式:
  • 使用 Mac 的网络共享功能将 Mac 的网络通过 WiFi 共享给 iPhone 连接;
  • 使用代理软件(例如 Charles)在Mac上建立HTTP代理服务器。
1.1、使用 Mac 的网络共享功能将 Mac 的网络通过 ‘WiFi共享’ 给iPhone连接
1.1.1、原理:

手机->运营商->服务器
手机->PC网卡->运营商->服务器

1.1.2、亲手操作:
  • 1、打开 系统偏好设置 -> 共享
  • 2、我设置了 Wi-Fi 为网络来源,共享给 iPhone USB 端口(因为只有一个网卡的原因,不能以 Wi-Fi 端口共享出去!);
  • 3、关闭 iPhone 上一切网络,用数据线连接到 mac USB 接口;
  • 4、神奇的事情发生了,没有WiFi和4G情况下,手机能正常上网;
开启Mac网络共享
开启Mac网络共享
wireshark监控刚刚我们共享网络的端口
wireshark监控刚刚我们共享网络的端口
wireshark抓到了iPhone的数据包(打开自己的APP请求)
wireshark抓到了iPhone的包

1.2、使用代理软件(例如 Charles)在Mac上建立HTTP代理服务器

1.3、Remote Virtual Interface,RVI

1.3.1、RVI介绍

苹果在 iOS 5 中新引入了“远程虚拟接口(Remote Virtual Interface,RVI)”的特性,可以在 Mac 中建立一个虚拟网络接口来作为 iOS 设备的网络栈,这样所有经过 iOS 设备的流量都会经过此虚拟接口。此虚拟接口只是监听 iOS 设备本身的协议栈(但并没有将网络流量中转到 Mac 本身的网络连接上),所有网络连接都是 iOS 设备本身的,与 Mac 电脑本身联不联网或者联网类型无关。iOS设备本身可以为任意网络类型(WiFi/xG),这样在 Mac 电脑上使用任意抓包工具(tcpdump、Wireshark、CPA)抓取 RVI 接口上的数据包就实现了对 iPhone 的抓包。

1.3.2、终端查看RVI
终端查看RVI
1.4.3、获取UUID
  • 首先,通过 USB 数据线将 iPhone 连接到安装了 Mac 上(老旧的设备可能不行),抓包过程中必须保持连接;
  • 然后,通过 iTunes->Summary 或者 Xcode->Organizer->Devices 获取 iPhone 的 UDID。
1.3.4、终端开启RVI

打开终端,使用“rvictl -s UUID”命令创建 RVI 接口,使用 iPhone 的 UDID 作为参数。

根据UUID开启RVI,并查看list
根据UUID开启RVI
wireshark监控RVI端口
wireshark监控RVI端口
wireshark抓到了iPhone的数据包(某车帝APP请求)
wireshark抓到了iPhone的数据包

二、牛刀小试 – 尝试抓某车帝的数据包

监控iPhone USB端口,打开某车帝APP某个页面。

设置快捷查找表达式查找HTTP请求
某接口
使用Firefox 的 RESTClient进行HTTP模拟请求
模拟HTTP请求

作者:lionsom_lin
链接:https://www.jianshu.com/p/82bcdb1decf7
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

Golang程序员开发效率神器汇总

一. 开发工具

1)sql2go
用于将 sql 语句转换为 golang 的 struct. 使用 ddl 语句即可。
例如对于创建表的语句: show create table xxx. 将输出的语句,直接粘贴进去就行。
http://stming.cn/tool/sql2go.html

2)toml2go
用于将编码后的 toml 文本转换问 golang 的 struct.
https://xuri.me/toml-to-go/

3)curl2go
用来将 curl 命令转化为具体的 golang 代码.
https://mholt.github.io/curl-to-go/

4)json2go
用于将 json 文本转换为 struct.
https://mholt.github.io/json-to-go/

5)mysql 转 ES 工具
http://www.ischoolbar.com/EsParser/

6)golang
模拟模板的工具,在支持泛型之前,可以考虑使用。
https://github.com/cheekybits/genny

7)查看某一个库的依赖情况,类似于 go list 功能
https://github.com/KyleBanks/depth

8)一个好用的文件压缩和解压工具,集成了 zip,tar 等多种功能,主要还有跨平台。
https://github.com/mholt/archiver

9)go 内置命令
go list 可以查看某一个包的依赖关系.
go vet 可以检查代码不符合 golang 规范的地方。

10)热编译工具
https://github.com/silenceper/gowatch

11)revive
golang 代码质量检测工具
https://github.com/mgechev/revive

12)Go Callvis
golang 的代码调用链图工具
https://github.com/TrueFurby/go-callvis

13)Realize
开发流程改进工具
https://github.com/oxequa/realize

14)Gotests
自动生成测试用例工具
https://github.com/cweill/gotests

二.调试工具

1)perf
代理工具,支持内存,cpu,堆栈查看,并支持火焰图.
perf 工具和 go-torch 工具,快捷定位程序问题.
https://github.com/uber-archive/go-torch
https://github.com/google/gops

2)dlv 远程调试
基于 goland+dlv 可以实现远程调式的能力.
https://github.com/go-delve/delve
提供了对 golang 原生的支持,相比 gdb 调试,简单太多。

3)网络代理工具
goproxy 代理,支持多种协议,支持 ssh 穿透和 kcp 协议.
https://github.com/snail007/goproxy

4)抓包工具
go-sniffer 工具,可扩展的抓包工具,可以开发自定义协议的工具包. 现在只支持了 http,mysql,redis,mongodb.
基于这个工具,我们开发了 qapp 协议的抓包。
https://github.com/40t/go-sniffer

5)反向代理工具,快捷开放内网端口供外部使用。
ngrok 可以让内网服务外部调用
https://ngrok.com/
https://github.com/inconshreveable/ngrok

6)配置化生成证书
从根证书,到业务侧证书一键生成.
https://github.com/cloudflare/cfssl

7)免费的证书获取工具
基于 acme 协议,从 letsencrypt 生成免费的证书,有效期 1 年,可自动续期。
https://github.com/Neilpang/acme.sh

8)开发环境管理工具,单机搭建可移植工具的利器。支持多种虚拟机后端。
vagrant常被拿来同 docker 相比,值得拥有。
https://github.com/hashicorp/vagrant

9)轻量级容器调度工具
nomad 可以非常方便的管理容器和传统应用,相比 k8s 来说,简单不要太多.
https://github.com/hashicorp/nomad

10)敏感信息和密钥管理工具
https://github.com/hashicorp/vault

11)高度可配置化的 http 转发工具,基于 etcd 配置。
https://github.com/gojek/weaver

12)进程监控工具 supervisor
https://www.jianshu.com/p/39b476e808d8

13)基于procFile进程管理工具. 相比 supervisor 更加简单。
https://github.com/ddollar/foreman

14)基于 http,https,websocket 的调试代理工具,配置功能丰富。在线教育的 nohost web 调试工具,基于此开发.
https://github.com/avwo/whistle

15)分布式调度工具
https://github.com/shunfei/cronsun/blob/master/README_ZH.md
https://github.com/ouqiang/gocron

16)自动化运维平台 Gaia
https://github.com/gaia-pipeline/gaia

三. 网络工具

四. 常用网站

go 百科全书: https://awesome-go.com/

json 解析: https://www.json.cn/

出口 IP: https://ipinfo.io/

redis 命令: http://doc.redisfans.com/

ES 命令首页:

https://www.elastic.co/guide/cn/elasticsearch/guide/current/index.html

UrlEncode: http://tool.chinaz.com/Tools/urlencode.aspx

Base64: https://tool.oschina.net/encrypt?type=3

Guid: https://www.guidgen.com/

常用工具: http://www.ofmonkey.com/

五. golang 常用库

日志
https://github.com/Sirupsen/logrus
https://github.com/uber-go/zap

配置
兼容 json,toml,yaml,hcl 等格式的日志库.
https://github.com/spf13/viper

存储
mysql: https://github.com/go-xorm/xorm
es: https://github.com/elastic/elasticsearch
redis: https://github.com/gomodule/redigo
mongo: https://github.com/mongodb/mongo-go-driver
kafka: https://github.com/Shopify/sarama

数据结构
https://github.com/emirpasic/gods

命令行
https://github.com/spf13/cobra

框架
https://github.com/grpc/grpc-go
https://github.com/gin-gonic/gin

并发
https://github.com/Jeffail/tunny
https://github.com/benmanns/goworker
现在我们框架在用的,虽然 star 不多,但是确实好用,当然还可以更好用.
https://github.com/rafaeldias/async

工具
定义了实用的判定类,以及针对结构体的校验逻辑,避免业务侧写复杂的代码.
https://github.com/asaskevich/govalidator
https://github.com/bytedance/go-tagexpr

protobuf 文件动态解析的接口,可以实现反射相关的能力。
https://github.com/jhump/protoreflect

表达式引擎工具
https://github.com/Knetic/govaluate
https://github.com/google/cel-go

字符串处理
https://github.com/huandu/xstrings

ratelimit 工具
https://github.com/uber-go/ratelimit
https://blog.csdn.net/chenchongg/article/details/85342086
https://github.com/juju/ratelimit

golang 熔断的库
熔断除了考虑频率限制,还要考虑 qps,出错率等其他东西.
https://github.com/afex/hystrix-go
https://github.com/sony/gobreaker

表格
https://github.com/chenjiandongx/go-echarts

tail 工具库
https://github.com/hpcloud/taglshi

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