0%

博客:cbb777.fun

全平台账号:安妮的心动录

github: https://github.com/anneheartrecord

下文中我说的可能对,也可能不对,鉴于笔者水平有限,请君自辨。有问题欢迎大家找我讨论

分布式核心要素

通常来说设计分布式系统的时候最需要考虑的核心要素有五个

  • Capacity 容量(能力) 指的是分布式系统里的CPU 内存 硬盘 网络 文件描述符 socket连接数等等硬性的指标
  • Perfomant 性能 指的是IOPS TPS QPS Latency Jitter之类的性能指标要求,性能受限于容量,性能同时又影响了可靠性以及可用性
  • Availablility 可用性 指的是产品或者服务在随机事件内调用时处于可服务状态的概率 也就是正常运行的时间/总时间 之前说的异地多活也是为了保证可用性而出现的
  • Reliability 可靠性 一般是不出故障的概率 通常企业级产品是5个9打底的 可以简单的和可用性划上约等号
  • Scalability 可伸缩性 指的是处理集群能否动态缩扩容  使得处理能力越来越多和越来越少的某种能力 系统的可伸缩性决定了该系统能不能伸缩

一个分布式系统通常会面临以下几个个难题:故障传播性、业务拆分与聚合、以及分布式事务

为了解决故障传播性的难题,我们可以采用”隔板” “熔断” “降级” “限流” “容错”以及”资源管控”等方式

微服务服务治理几大模式

隔板模式

场景:
在分布式系统中通常将进程容器化 以进行资源隔离,然后在同一个进程中的所有业务都共享线程池,对外提供服务,但是这就导致了会经常遇到这样的问题:

  1. 业务A负载较高,抢占了线程池里的大部分线程资源,从而导致其他业务的服务质量下降
  2. 同一个进程内新加一个业务,这个业务会抢占其他业务的资源,可能会造成系统的不稳定,比如业务性能抖动
  3. 难以调试,多个业务共享一个线程池,当出现故障的时候很难通过简单的日志判断是哪个业务出了问题

隔板模式:在分布式系统里进行资源的隔离,资源隔离通常按照业务粒度分为进程级别和线程级别

进程隔离:通常使用的是容器化进行隔离,比如通过docker实现业务进程之间的资源隔离,底层就是通过namespace实现的操作系统级别的隔离,比如隔离进程、网络、通信等等。cgroup实现的硬件层面的隔离,比如CPU、内存等等。具体实现笔者之前的博客有提到

线程隔离:指给每个跑在进程里的业务按照业务类型创建一个线程池,从而实现线程级别粒度的资源隔离

优势:

  • 提高业务可靠性,减少业务受其他业务影响的程度,当一个业务耗尽自身资源后也不会影响到其他业务的服务质量
  • 降低新加入的业务给系统带来的风险,减少新加业务导致其他业务可能出现的性能抖动
  • 利于调试,通过线程池可以很方便的定位是哪个服务出了故障,并且可以通过监控线程池的请求失败次数、超时次数、拒绝请求次数等可以实时反应当前的业务质量

劣势:

粒度更细,很容易就能想到劣势是引入了额外的开销,具体开销的点如下

  1. 对象分配 创建多个线程对象
  2. 并发 可能会有一些竞态问题 为了避免竞态问题 则必须进行并发控制
  3. 线程的切换开销 操作系统层面的开销

这些开销对于整个系统或者业务来说,一般开销不会特别大,在一些要求不苛刻的场景可以忽略

微服务限流三大件:熔断、降级、限流

熔断模式

场景:

1.系统负载突然过高,比如突发的访问量、过多的请求以及IO压力过载都可能会造成某个节点故障,比如节点A,然后节点A挂了,又把负载传给节点B,节点B负载过高之后又挂了,这样一连串的挂过去就会把请求从单点故障转化成为系统级别的级联故障

2.我们希望在一个服务出现故障的时候,能够在一段时间内恢复,在请求被拒绝一段时间后再自动的去探测服务的可服务性

熔断模式:也称为断路器模式,当系统里的响应时间或者异常比率或者单位异常数超过某个阈值的时候,比如超时次数或者重试次数超过某个阈值,就会触发熔断,接着所有的调用都快速失败,从而保证下游系统的负载安全。

在断开一段时间之后,熔断器又试着让部分请求负载通过,如果这些请求成功,那么断路器就恢复正常,如果继续失败,那么就关闭服务,立刻返回失败,接着继续这个过程直到重试的次数超过一定的阈值,从而触发更加严重的”降级模式”

具体过程如下

  • 熔断器开始处于闭合状态,如果达到触发条件,那么熔断器就会打开
  • 接着熔断器处于打开状态,所有走到这个路径里的请求会走快速失败通道,从而避免负载下行,给下游的服务造成压力,过一个时间周期之后会自动切换到半打开状态
  • 半打开状态:认为之前的错误可能已经被修复了,因此允许通过部分请求试着看看能不能处理成功,如果这些请求处理成功,那么就认为之前导致失败的错误已经被修复,此事熔断器就切换到闭合状态,并且将计数器重置。如果这些试着发送的请求还是失败,则认为之前的问题没有解决,熔断器切回到打开模式,然后开始重置计数器给系统一定的时间来修复错误
  • 接着重复以上过程,直到半打开状态重复的次数达到一定的阈值发现错误还没被修复,从而触发”降级”状态

降级模式

场景:

1.某些时候系统会遇到负载过高的问题,当系统外来的或者内部的负载过高,超过预先定义的阈值,为了保证更加重要的业务的服务质量,希望将一些非核心的业务降低服务质量,从而释放一些额外的资源给紧急业务使用。
比如一个分布式系统里的读、写、数据校验、空间回收都比较消耗资源,在业务高峰期为了保证读和写的服务治理,可以把数据校验的服务通过限流或者减少线程数之类的方式,使得该服务能够调用的资源配额减少,从而释放部分资源给读和写使用,保证读写的服务质量。

同样,在读和写业务不繁忙的时候,降低业务的资源配额,从而释放资源给空间回收使用。通过这种方式动态调整局部业务的服务质量从而保证关键业务的服务治理,提升用户体验。

2.在云服务中”可用性”是一个很重要的指标,所以我们希望分布式的系统尽可能稳定,不管出现怎么样的故障,都能过保持基本的可用性

降级模式

可以从故障处理和系统服务质量两个角度理解降级模式

从故障处理角度来说,服务降级就是这一功能或者服务直接不可用。

而在动态调整系统整体的服务质量的时候,降级是降低某些当前非重要或者非核心业务的资源,从而释放部分资源给重要的或紧急的业务使用

故障处理:是比熔断更加严重的故障处理方式,最后拿来兜底用的。

比如某个功能出故障,”熔断”是还有希望将这个功能救活。而”降级”是发现救了几次没活之后,直接砍掉这个服务,保证服务整体不出问题

系统服务质量:分为读功能降级、写功能降级、级联组件降级,还有自动降级或者人工降级。比如在云服务里,为了保证高可用性,在出现系统级的故障后,可以把写功能降级,就是这个服务状态变为只能读、只能查询而不能写。

因此在设计的比较好的云服务里,按时间的纬度来度量可用性已经没有了太大的意义,因为不管怎样服务都是可用的,系统都是活着的,起码部分服务可用,因此在云服务里更合理的新的衡量可用性的指标是请求失败比率,即哪些服务不能对外提供能力,占比具体为多少

降级设计思路

触发策略

  • 超时降级:在超时重试的次数达到一个阈值后就触发降级
  • 失败比率重试:当某个服务的失败比率达到一定比率后开始降级
  • 系统故障降级:比如网络故障,硬盘故障,电源故障,服务器故障,数据中心故障等等
  • 限流降级:某些访问量太大的场景会触发限流,当达到限流的阈值后,请求也会被降级
  • 重要业务救急:比如为了保证读或者查询的功能,降低写、数据校验的资源配额

降级处理

  • 资源配额调度,调度不紧急的业务支援紧急的重要的业务
  • 抛出异常,直接抛出异常,打印出错误日志,然后就不管了,请求会丢失,这在需要保证幂等性的请求里不太合适
  • 直接返回,直接返回拒绝服务,这里请求也会丢失,这在需要保证幂等性的请求里不太合适
  • 调用回退方法,调用出现服务降级时对应的业务处理逻辑,不同场景降级的逻辑不一样,比如可以把请求挂在等待队列里继续重试之类,这里需要根据业务场景合理设计回退方法

服务降级策略

可以把降级的等级分为几个层次,比如PO P1 P2 P3等等,等级越高表示问题越严重

1.重要业务救急降级可以定义为P0级,只是调度次要的资源去救急,不会出现故障

2.限流降级可以定义为P1 ,只是为了保证服务质量,而且如果不限流可能会出现系统负载过高从而出现故障

3.超时/失败比率降级以及失败比率可以定义为P2 出现小范围故障 而不蔓延不传播

4.系统故障级别可以定义为P3级别 此事可以只保证最低资源的读请求服务


通常来说,分布式系统中每个服务的配置信息会保存在一个配置中心里,这个配置中心里可以有有每个服务的开关信息以及一些重要的资源配置信息。通过动态调整服务的配置信息,比如降级触发策略、降级处理措施、降级分级策略等来实现服务降级功能。

分布式配置中心:管理各个服务的各种配置信息,包括但不限于以下内容

  1. 应用程序的基本配置参数,如数据库连接信息、日志级别、缓存配置等等
  2. 服务之间的调用配置,如远程服务的地址、超时设置、负载均衡策略等等
  3. 业务规则的配置,如业务策略、规则、权限等等
  4. 动态特性的配置,如开关、AB测试等等
  5. 系统的降级策略配置,如降级的规则,降级处理的方式等等

具体的实现可以如下,在配置中心定义一个降级策略的配置项,然后在系统中读取该配置项并根据其值进行对应的降级处理。

参照上文,可以定义一个DegradeStrategy配置项,值为NULL P0 P1 P2 P3几个常量中的一个,在程序代码中读取加载该配置,分为以下几种情况进行请求的处理

  • NULL 不进行降级处理
  • P0 走业务备用逻辑
  • P2 采用模拟的数据 或者缓存的数据进行响应
  • P3 直接返回错误

限流

动机:

可靠性:每个系统都有自己的容量限制,也就是说能够处理的业务请求能力是有限的,如果不控制这些输入的请求数,突发输入过多的请求量会造成过度的资源竞争从而引发系统故障降低系统的可靠性

可用性:限流有利于控制系统资源的消耗速率,有利于过载保护保证业务资源不被耗尽

流量监管:对输入的请求流量进行细粒度的控制,通过监管输入的请求量速率对超出的部分进行惩罚,比如直接丢弃,使得进入系统的请求量被限制在一个系统所能承受的合理的范围之内,流量监管比较适合对延时要求较高的业务

流量整形:控制最大输出请求速率,以确保请求量符合系统容量配置的最大传输速率规定。请求的流量被整形进行平滑处理,以使它符合下游服务的速率需求,流量整形比较适合可靠性要求较高的业务

流这个词在不同上下文的含义是不一样的

  • 网络限流:带宽、流量
  • IO限流:TPS QPS
  • 并发限流:并发请求数
  • 线程限流:线程数

限流处理策略

  • 直接拒绝:当请求量超过阈值之后,新的请求就会被直接拒绝,方式为直接返回或者抛出异常。这种方式比较适合对分布式系统的负载容量已知的情况下,比如通过全链路压测已经确定了准确的系统处理能力以及系统容量
  • 冷启:当分布式系统长期处于低负载的情况下,请求量突发的时候,会把请求负载很快拉到很高的水准,这样可能瞬间就把系统击垮。通过”冷启”的方式,让输入的请求量缓慢增加,慢慢增加到阈值附近,对应的是令牌桶算法
  • 匀速排队:系统流量均匀,对应漏桶算法

纵向限流

两窗算法+两桶算法(固定窗口、滑动窗口)+(令牌桶、漏桶)
按照工作原理又可以划分为 保险丝模式和变压器模式

保险丝与两窗算法

保险丝:
电路中的保险丝主要是起电流过载保护作用,当电路中的电流过载的时候,保险丝自身就会烧坏从而切断电流,保护后续电路的安全运行。

这与限流算法中的窗口算法原理类似,在拒绝请求之后,需要重新设置计数,因此我们定义它们为限流保险丝模式

固定窗口:
按照时间线划分成一个个固定大小的时间窗口,并且每个窗口都有一个计数器来统计这一时间窗口内的访问次数,如果访问的次数超过了一个预先定义的阈值,则拒绝接下来的请求。直到下一个时间窗口,开始重新计数,当计数器又超过则继续拒绝,再在下一个时间窗口重新设置计数器继续计数,依次类推……

优点: 实现简单 一个计数器就可以实现

缺点有边界场景和跨窗口场景两个点,前者导致流量不均,可能有时候无法处理某些请求;后者导致流量可能超过阈值而带来风险

边界场景:

在第一个[0,5]的时间窗口内,第一秒就把计数器打到超过500,则后续的四秒将无法服务,得等到下一个[5,10]的时间窗口内计数器才被重置为0,才可以对外提供服务

跨窗口场景:

当第一个时间窗口的[4,5]的计数器为300,没有超过阈值,然后第二个时间窗口的[5,6]计数器为320,也没超过阈值,但是在[4,6]的时间窗口内计数为620 超过阈值,可能带来风险

滑动窗口:

也类似于固定窗口的计数器,不过将窗口按照时间线做了进一步的划分,每次往后移动一个细分单元,再每次都对一个小窗口进行计数统计实现流量控制。比如刚刚上图,把窗口的大小从5S缩小到1S,且会自动按照时间线进行移动

能很好的规避掉跨窗口场景 但是对边界场景还是会不太平滑 不过也比固定窗口好很多了

变压器与两桶算法

变压器指的是将电路中某一等级的电压或电流转换成另外一种同频率的电压或电流的设备,有利于稳流稳压,在计算机中对应的是两桶算法,即漏桶和令牌桶

漏桶



漏桶算法工作步骤

  • 请求被随意的输入,有突发较多的请求量也有较小的请求量,这些请求进入系统之后不是立马被处理,而是放在一个桶中
  • 当桶了缓冲的请求超过设置的水位时,输入的请求被拒绝进入,直接丢失
  • 桶以恒定的结果将输入的请求输出

优点:
有利于流量的削峰填谷,且输出总是按照恒定的速率输出,因此有利于流量整形,平滑了突发的请求量

缺点:

1.无法接收突发流量 如果有超过桶设置水位的突发流量会被抛弃 这在幂等性的场景中明显是不适用的 比如支付场景 可能导致支付请求的丢失

2.因为漏桶总是按照恒定速率输出请求(也不是每时每刻都以该速率输出 当某时刻小于到达的请求量设置的输出值的时候 则会比设置值小),这是在假设后续的服务能够承接这个速率的前提之下的,如果无法保证这些输出的请求稳定的在一个固定的时间内处理完,可能会导致后续的服务进行资源抢用,而导致引发更大的级联故障

令牌桶


工作步骤

  • 这个桶每段时间会生成N个令牌
  • 桶子的最大令牌数量有限制
  • 如果有请求到来,则必须先在桶子里面拿令牌,然后进行请求的处理,之后从这个桶子里把令牌删掉
  • 如果桶子里面没令牌,当前请求无法通过,之后重试

优点:

  • 当桶子里的令牌满了,是丢令牌而不是丢请求,这样可以在幂等性请求的场景使用
  • 可以支持突发的流量

缺点:
对输出的请求速率没有做限制,有可能会打崩整个系统

算法实践:

  • 两窗算法实现比较简单,性能好,但是超出限流阈值之后会直接拒绝请求,适用于非幂等的请求场景
  • 漏桶算法,平滑控制输出的请求速率,但是超出水位的请求会被丢弃,适用于非幂等的请求场景
  • 令牌算法,可以支持突发的请求量,不控制输出的请求速率;超出阈值之后只会丢失令牌但不丢失请求,可以结合在幂等性请求的场景使用

横向限流

纵向限流解决的是某一个服务,一条链路的流量过高的问题,但是并没有解决这几个服务路径之间流量是否均匀分配的问题

横向限流的作用

  • 解决限流不均匀问题,尽可能让每个服务之间的流量是均匀的
  • 更细粒度的用户限流问题 限制每个用户(租户)可以进入系统的请求个数,纵向限流只能限制整体的进入网关的请求数,因此需要一个计数中心用于登记每个用户的请求数,从而进行更细粒度的流量控制,控制每个用户的请求数

通常是通过一个类似配置中心的方式实现横向限流

  • 可以将集群限流服务中心实现在一个网关实例里,与网关一起提供服务,好处是不需要再独立部署一个限流实例,缺点是如果网关挂掉,那么限流服务会一起挂掉,而且无法对网关层面进行横向限流,只实现了各个网关底下的服务的横向限流
  • 也可以独立拉起一个集群限流服务中心实例,用于提供全局限流计数服务,好处是与业务解耦,缺点是在集群内增加了一个额外的服务实例,增加了系统复杂度

常见的横向限流算法有计数算法以及时间标签算法

计数算法

拉起一个独立的分布式配置中心,在里面实现限流算法,比如两窗、两桶算法用于全局计数,而且保证这个计数是全局唯一的,不管集群规模多大,保证每个服务所使用的计数器和计时器都是唯一的,服务拿到这个计数ID之后再进行限流调度

CP模式:采用独立的限流中心,每个用户进入系统的请求都需要去远程的限流服务中心取一个计数返回,多了一个远程读取限流计数值的过程,会比较影响请求的性能
AP模式:本地维护一个限流技术的缓存,起一个独立线程维护,每隔一段时间本地限流缓存和远程进行同步,这种方式牺牲了限流的可靠性,但是保证了请求的性能

时间标签算法

计数算法只是实现了限制用户的请求量的最大值,并不能提供最小值保证,于是基于时间标签的算法被提出

例如在云服务中,用户1和用户2付费不一样,因此提供的最大限流上线是不一样的,但是如果采用计数算法的话并不能保证付费多的用户就一定能得到更高的服务质量保证。因此需要一个可以预留资源的算法


思路为:先保证最低的预留值,再根据权重划分剩下的资源,并且保证不要超过最大值。

例如在云服务中,用户1和用户2付费不一样,因此提供的最大限流上线是不一样的,但是如果采用计数算法的话并不能保证付费多的用户就一定能得到更高的服务质量保证。因此需要一个可以预留资源的算法


思路为:先保证最低的预留值,再根据权重划分剩下的资源,并且保证不要超过最大值。

CAP理论

CAP是分布式系统方向中的一个非常重要的理论,可以粗略的将它看成是分布式系统的起点,CAP分别代表的是分布式系统中的三种性质,分别是Consistency(可用性)、Availability(一致性)、Partition tolerance(网络分区容忍性),它们的第一个字母分别是C A P,于是这个理论被称为CAP理论

理论上来说,CAP三者同时最多满足两者,但是并不是必须满足两个,许多系统最多只能同时满足0、1个

为什么CAP最多只能满足两个呢?

我们可以以电商系统的两个集群来当做例子

C: 追求的是数据一致性 当有一个请求来了之后 它会等待网络隔离的情况结束之后 向另一个机器进行数据的同步

A: 追求的是可用性 也就是尽可能提供有效服务 当一个请求来了之后 它会立即返回 哪怕数据是陈旧的 也得优先提供服务,其他分区的节点返回的结果(数据)可能是不一样

注意:这里的AP不可同时满足指的是当整个分布式系统中出现网络隔离的时候,我们不能既想着保证数据的实时强一致性,又去追求服务的可用性。

但是当没有网络隔离的时候,其实这两个性质是可以同时满足的,因为『同步数据』和『返回结果』这两个操作都是在同一个网络中,只有先后关系,不会因为某个操作导致另一个操作的『死等』

在分布式系统中,P是会必然发生的,造成P的原因可能是网络隔离,也可能是节点宕机。我们无法保证分布式系统每一时刻都不出现网络隔离,如果不满足P的特性,一旦发生分区错误,那么分布式系统就无法工作,这显然违背了分布式的理念,连最基本的分布式系统条件都没有满足

典型的CP和AP的产品

CP:Zookeeper 当系统在发生分区故障之后 客户端的所有请求都会被卡死或者超时 但是系统总会返回一致的数据

AP:Eureka 分区发生故障之后 客户端依然可以访问系统 但是获取的数据有的是新数据 有的是老数据

当然 CAP这几个特性不是BOOL类型的,而是一个范围类型,完全是看系统具体需要什么样的要求。

比如分区容错,有的系统一台机器出错,系统会认为不影响业务的话,认为分区不存在。只有多台机器都出问题了,系统受到严重影响才认为出现分区

PACELC理论

PACELC理论是对CAP理论的扩展,在维基百科上的定义是

1
It states that in case of network partitioning (P) in a distributed computer system, one has to choose between availability (A) and consistency (C) (as per the CAP theorem), but else (E), even when the system is running normally in the absence of partitions, one has to choose between latency (L) and consistency (C).

如果有分区(P),那么系统就必须在可用性(A)和一致性(C)之间取得平衡,否则(E),当系统运行在无分区的情况下,系统需要在延迟(L)和一致性(C)之间取得平衡

它相比于CAP,多引入了一个延迟Latency的概念,在出现分区错误的时候,取前半部分PAC,理论和CAP的内容一致。没有出现分区错误的时候取LC,也就是Latency与Consistency

当前分布式系统指导理论更替代CAP理论,理由如下

  • PACELC更能满足实际操作中分布式系统的工作场景,是更好的工程实现策略
  • 当P存在的场景下,需要在A C之间做取舍,但是实际上分布式系统大部分时间里P是不存在的,那么在L和C之间做取舍是一个更好的选择
  • PACELC可以在latency与consistency之间获得平衡

要保证系统的高可用,那么就得采用冗余的思想,我的其他博文有提到4个9的异地多活策略,也是采用的数据冗余思想,而一旦涉及到了复制数据,在分布式中就一定会在Consistency和Latency之间做一个取舍

img

举个例子

在强一致性的场景下,需要三个从节点都落盘数据,才能给客户端返回OK,这个时候当master向slave同步数据的时候,超过20ms触发超时了,整个系统还是会不断的重试这个过程,这显然造成了系统的可用性比较低

所以我们一般都会在数据一致性和请求时延之间做一个balance

当同步超过五次之后,认为这个节点故障,选择直接返回,可以消除写时的长尾抖动,同时给节点打上故障标签,进行后续的处理

BASE理论

base理论是Basically Avaliable(基本可用)、Soft State(软状态)、Eventually Consistent(最终一致性)三个短语的缩写,核心思想如下

即使无法做到强一致性,但每个应用都可以根据自身的业务特点,采用适当的方式来使系统达到最终一致性

BA:基本可用指的是当系统出现了不可预知的故障,系统依旧可用,不过可用度也许会降低,比如响应时间上出现损失,功能上只能满足基本功能等等

S:基于原子性而言的话,当要求多个节点数据一致时,我们认为这是一种『硬』状态,而允许系统中的数据存在中间状态,并认为其不影响系统的整体可用性,即允许系统在多个不同节点的数据副本存在数据时延

E:最终一致性,系统不可能一直都处于一个软状态中,必须有个时间期限。在期限过后,应该保证所有副本保持数据一致性,从而达到数据的最终一致性。这个时间期限取决于时延、负载、方案等等

在工程实践中,有这么几种最终一致性的实现策略,通常都是多种策略混合实现

  • 因果一致性:如果节点A在更新完某个数据后通知了节点B,那么节点B之后对该数据的访问和修改都是基于A更新后的值。与此同时,与节点A无因果关系的节点C的数据访问没有这样的限制
  • 读已知所写:节点A更新一个数据之后,自身总是能访问到更新过的最新值,而不会访问旧值
  • 会话一致性:将对系统数据的访问过程框定在了一个会话当中,系统能保证同一个有效的会话中实现客户端在一个会话中读取到该数据项永远是最新值
  • 单调读一致性:如果一个节点从系统中读取出一个数据项的某个值之后,那么系统对于该节点后续的任何数据访问都不该返回更旧的值
  • 单调写一致性:一个系统要能够保证来自同一个节点的写操作被顺序的执行。

NWR多数派理论

NWR多数派理论指的是在多数副本的一致性模型中,只有大多数副本确认了某个操作,才认为这个操作已经完成。这个理论是分布式系统中一种常见的一致性模型,被广泛应用于保证数据的一致性和可靠性,以及系统的可用性。

NWR中N代表的是副本数量,W代表写入的副本数量,R则为读取的副本数量。在多数的一致性模型中,一般要求W+R>N,以保证读写操作的一致性。在写入操作的时候,只有W个副本被成功写入才返回成功,而在读取操作时,只有R个副本成功返回相同的数据才返回成功。这样,只要大多数副本成功确认了操作,就可以认为这个操作已经完成。

NWR在现有组件的应用还是很广泛的,比如Raft选主判断逻辑为投票数量>=n/2则成功选主,比如Redis的哨兵机制,有哨兵标记下线则为主观下线,>=n/2标记下线则为客观下线。

博客:cbb777.fun

全平台账号:安妮的心动录

github: https://github.com/anneheartrecord

下文中我说的可能对,也可能不对,鉴于笔者水平有限,请君自辨。有问题欢迎大家找我讨论

什么是视频资源

所谓视频资源,在播放和底层存储的时候其实就是一张一张图,30帧为一秒三十张图,60帧为一秒60张图等。一连串的图片按照一定帧率播放出来,刚开始的时候没有声音,所以音频信息就需要在各个帧进行精确设置,播放才能够音画同步。

流媒体:

计算机科技在历经了文件(File)、页面(Web)时代之后,今天来到了以流(Streams)为单位的时代,而这个流的最典型的应用就是“流媒体”技术,它指的是在网络上采用流式传输技术来发布音频、视频以及其他多媒体文件。

所谓的流式传输就是将音频、视频多媒体文件经过一定的算法,编码压缩成一个个很小的压缩包,流媒体服务器通过特定网络协议进行连续、实时的传送,用户端接受到压缩包后由播放软件实时解压缩实现播放的过程。而所谓的流媒体则特指一切采用这种流式传输的媒体文件。

今天我们通过互联网看视频、直播、盒子看电视算如此方便,背后就有着流媒体系统的支撑。

在对于音视频的传输分发上,我们会经常听到HTTP渐进式、HTTP流、文件下载、实时流式传输等等专业术语,下表总结了不同传输方式的特点

image.png

需要完成一个支撑高体验大规模的流媒体系统,需要考虑的技术层面有

  1. 云计算基础服务相关技术:基于云架构的计算、网络、存储、CDN等底层基础服务已经变成了必须。硬件虚拟化、网络虚拟化能够最大程度保障音视频播放的稳定性;同时CDN内容分发网络能够有效应对高并发和徒增流量的需求,对流媒体传输的所有环节进行针对性优化,大幅度降低延时,对象存储满足了流媒体数据的大规模存储要求

  2. 音视频相关技术:音视频的编解码、4K、VR等音视频核心技术能力

  3. 场景化需求:秀场娱乐直播的实时录制,实时水印,实时鉴黄;连麦,版权保护等等

Youtube

Youtube是如何存储海量音视频数据的?

Youtube是仅次于谷歌的第二大热门网站,在19年5月,每分钟会有500小时的视频内容上传到该平台

架构

image.png

Youtube的后端微服务是由Python Java 和Go 写的,而前端是使用JS写的,主要的数据库是由Vitess支持的MySQL,另外使用Memcache实现了缓存,并使用Zookeeper进行节点的协调

流行的视频通过CDN来提供,而一般的、较少播放的视频则从数据库中获取

视频压缩:能够使用其他编码器一半的带宽来编码视频

视频流:使用基于HTTP协议的动态自适应流,可以按照不同的速率提供给观众,客户端通过观看者的互联网速度自动适应视频渲染,从而尽可能少的减少缓冲时间

Vitess的诞生

随着网站越来越大,请求量越来越多,不得不对数据库进行水平拓展

主-从副本

副本会添加到数据库实例中,读取请求会被路由到主数据库和副本上,其中主节点可写又可读,从节点只可读。

但是这种场景中,有可能会从副本中读取到陈旧的数据,如果在主节点将信息更新到副本之前,一个请求读取了副本的数据,那么观看者就会得到陈旧的数据。但是一般没什么大问题,因为在一段时间的运行后,不同节点的数据会答到最终一致

分片

当QPS继续增大,就该对数据库进行分片了,分片并不是一个简单的过程,它大大的增加了系统的复杂性

数据库分片之后,数据被分散到多台机器上,这增加了系统写入的吞吐量,不再是只有一个主节点能承担写的任务,同时,每台机器都创建了单独的副本,以实现冗余和吞吐

灾难管理

为了防止突然掉电、火灾等情况,需要对数据进行冗余,将用户数据备份到世界不同地理区域的数据中心。丢失用户数据和服务不可用算不允许的。同时拥有多个数据中心也有助于Youtube减少系统延迟,因为用户请求会被路由到最近的数据中心

Vitess

是一个运行在MySQL之上的数据库集群方案,能够使MySQL进行水平拓展。它具有内置的分片特性,能够让开发人员拓展数据库,而不必再应用中添加任何的分片逻辑。类似于nosql的做法

image.png

vitess会自动处理故障转移与备份,除了youtube,vitess还被业界的其他知名厂商使用,如github等

当你需要acid事务和强一致性的支持,同时又希望像NoSQL一样快速拓展关系型数据库的时候,Vitess就会大显身手,通过基于go编程提供的连接池,Vitess能够以很低的成本管理大量连接

部署到云中:vitess是云原生的,并且容量是逐步添加到数据库中的。它可以作为一个K8S感知的云原生分布式数据库运行

如何存储

视频会存储在谷歌数据中心的硬盘里,由GFS和BigTbale管理, GFS是谷歌开发的一个分布式文件系统,用于管理分布式环境中的大规模数据,而BigTable是建立在GFS上的低延迟分布式数据存储系统,用于处理分布在千万台机器上的PB级别的数据

博客:cbb777.fun

全平台账号:安妮的心动录

github: https://github.com/anneheartrecord

下文中我说的可能对,也可能不对,鉴于笔者水平有限,请君自辨。有问题欢迎大家找我讨论

计算机启动过程

基本围绕着输入输出系统(Basic Input/Onput System BIOS)展开

1.开始供电

2.Power On Self Test 检测关键设备是否存在,能否正常运行。如果系统BIOS在这个过程中发现了一些致命错误,比如内存出错或者没有内存,那就会直接控制喇叭来报告错误

3.BIOS选择启动盘 F12可进入选择页面,比如双系统就要这个阶段选择启动的是哪个操作系统

操作系统启动过程

1.读取硬盘的前512个字节,确定操作系统在磁盘中的位置

2.将操作系统载入内核中

程序的生成和运行

Java程序:将源代码通过编译器编译成.class类型文件(字节码),这种文件只能通过JVM识别。

运行的时候JVM从.class文件中读一行解释执行一行。Java为了实现跨平台,不同的操作系统对应不同的JVM,也就是说Java前半部分是编译,后半部分是解析,是一种混合型程序

一个C程序的编写到运行过程

  1. 首先使用编辑器编辑程序源文件(.c .cpp)

  2. 源程序经过编译器被编译为等价的汇编代码,再经过汇编器产生与目标平台CPU一致的目标代码,也就是机器码

  3. 尽管目标代码的指令已经可以被目标CPU执行了,但是可能还包含没有解析的名称和地址引用,因此需要连接器把目标代码文件和其他的一些库文件及资源文件连接起来,产生可执行的二进制exe文件

  4. 当执行.exe文件的时候,windows操作系统会解读链接器记录再可执行程序中的格式信息,然后将代码和数据“放置”在内存中,成为可以运行的内存映像,然后生成一个进程开始运行

编译过程

编译器(汇编器)所作的工作主要是翻译工作

  1. 预处理,正式编译之前,通过文件中的预处理指令来修改源文件的内容,包括宏定义指令、条件编译指令、头文件包含指令和特殊符号替换指令等等

  2. 编译和优化,通过词法分析、语法分析、语义分析,将其翻译成等价汇编代码,并对其进行优化处理

  3. 目标代码生成,将中间代码生成符合当前机器CPU的机器码

预处理指令

1.宏定义指令: #define,这是最常见的用法,可以定义符号常量,函数,重命名等

2.条件编译指令:#if #ifndef #ifdef #elif 等等,主要进行编译时有选择的一些代码,比如#ifdef 如果宏已经定义,则编译下面代码

3.头文件包含指令:#inlcude 是一种最为常见的预处理,主要是作为文件的引用组合源程序正文

4.特殊符号处理:可以识别一些特殊的符号,预编译程序对于在源程序中出现的这些串将用合适的值进行替换

程序的链接

链接器的基本功能是:将编译器产生的多个目标文件合成为一个可以在目标平台下运行的文件,这里说的目标平台指的是程序的运行环境,CPU和操作系统等

链接器按照工作模式分为静态链接和动态链接两类

1.静态链接 链接器将函数的代码从其所在地(目标文件或者静态链接库)复制到最终的可执行程序中,整个过程在程序生成的时候完成,静态链接库实际上是一个目标文件的集合,其中的每个文件含有库中的一个或者一组相关函数的代码,静态链接则是将相关代码复制到源代码相关位置,参与程序的生成。也就是去静态链接库(文件库)中找代码,复制到最终的可执行程序中

2.动态链接 动态链接库在编译链接的时候只提供符号表和其他少量信息,用于保证所有符号引用都有定义,保证编译顺利通过。程序执行的时候动态链接库的全部内容将被映射到内存中,根据可执行程序中的信息找到相应的函数地址并调用执行。也就是去动态链接库中全部内容会被映射到内存中,程序通过一些符号表和其他信息找到对应的内容,并进行调用。

逆向分析

逆向就是在不能轻易获得必要的生产信息下,直接对成品的分析入手,推导出产品的设计原理,主要分为硬件和软件两大部分

动态分析

将一个目标代码变换为易读形式的过程,一般是在调试器或者工具中加载程序,然后一边运行程序一边对程序的行为进行观察和分析,这些调试器或者调试工作包括:IDE,著名工具OllyDbg等等

缺点:

- 严重依赖输入

- 可能因为环境原因,无法运行目标程序

静态分析

不执行代码,而是通过反汇编、反编译等工具,将程序的二进制代码翻译成汇编语言,之后再进行分析。常用工具有IDA Pro,C32Asm等等

缺点

- 程序加壳之后无法反汇编

- 代码混淆甚至于被加密处理

软件逆向的一般过程

  1. 文件装载进行与目标文件相关的一些初步分析,包括文件格式解析、文件信息搜集和文件性质判定,分析出文件执行入口地址,初步分析文件的数据段和代码段以及文件运行所以来的其他文件信息

  2. 指令解码指令解析,将指令机器码影视各位成为汇编语言

  3. 语义映射通过直接代码实现或者工具实现

  4. 相关图构造

  5. 过程分析

  6. 类型分析

  7. 结果输出结果输出时逆向分析的最终阶段

周末想仔细了解一下http传参,包括GET方法的传参、POST方法的传参,索性把GIN也捡起来了一点

基础

1.Gin.Use和Gin.Default的区别

1
2
//Default加了两个中间件
Gin.Use(Logger(),Recovery())

有打印日志和从Panic中恢复的能力
Logger中间件会将日志写入gin.DefaultWriter,即使配置了GIN_MODE=release
Recovery中间件会recover任何panic,如果有panic的话,会写入500响应码
2.Gin怎么分组
gin.Group对路由进行分组

参数处理

Gin怎么拿到URL中的参数
用ID举例子
Param

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
C.Param("id")

// 这个时候路由要这么写 id就成为了不定参数
GET("/:id")


//这时候url中的请求格式就应该如下
http://localhost:8080/id=xx
//如果有多的参数则应该是
http://localhost:8080/id=xx&name=xx
GET("/:id/:name")

//当我们想处理URL中所有的参数的时候
//比如说有name id type等等
//这个时候路由就该这么写

c.Param("all")
GET("/*all")

在URL中传参时必须传某个param该怎么做?

1
2
3
4
5
6
7
8
9
10
type Produdct struct {
ID int `uri:"id" binding:"required"
Name string `uri:"name" binding:"required"
}
// uri也是tag支持的一个键值对之一


var p Product
err := c.ShouldBindUri(&p)
// 如果这里err有问题了,那么很显然就是某个param没有传

struct tag

在很多项目代码里面,很容易看到有一些结构体的定义是类似下面这个结构体的

1
2
3
4
type Location struct {
Longitude float32 `json:"lon,omitempty"`
Latitude float32 `json:"lat,omitempty"`
}

字段后面会有一个标签,这个标签通常是由反引号给括起来的
Go提供了可通过发射发现的结构体标签,这些在标签库json/xml中得到了广泛的使用,orm框架也支持了结构体标签,上面的这个例子就是因为encoding/json支持json结构体标签,每种标签都有自己的特殊规则
不过所有标签都遵循一个总体规则,这个规则是不能更改的,具体格式如下
key1:"value1" key2:"value2" key3:"value3"...
结构体标签可以有多个键值对,键与值要用冒号分割,值要使用双引号括起来,多个键值对之间使用一个空格进行分割
而一个值中要传递多个信息,不同库的实现是不一样的,在encoding/json中,多值使用逗号进行分割
例如下面的例子
json:"lon,omitempty"
在gorm中,多值使用分号进行分隔
``gorm:”column:id;primaryKey” 结构体标签的具体作用时机如下 **在编译阶段和成员进行关联,以字符串的形式进行关联,在运行阶段可以通过反射读取出来** 在Go项目的编译阶段是不会对struct tag做合法键值对的检查的,如果我们不小心写错了,就会很难被发现,这个时候我们就可以使用go vet`工具,帮助我们做CI检查
下面是Go支持的struct tag类型
image.png
image.pngimage.png

自定义结构体标签

结构体标签是可以随意写的,只要符合语法规则。但是一些库没有支持该标签的情况下,随意写的标签是没有任何意义的,如果想要我们的标签变得有意义,就需要我们提供解析方法。可以通过反射的方法获取标签,下面是一个例子

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
type test struct {
Name string `cheng:"name"`
ID int `cheng:"id"`
Type int `cheng:"type"`
}

func getStructTag(obj interface{}) {
t := reflect.TypeOf(obj)
for i := 0; i < t.NumField(); i++ {
value := t.Field(i)
tag := value.Tag.Get("cheng")
fmt.Println("get tag is", tag)
}
}

func main() {
t := test{
Name: "yuyating",
ID: 2021212345,
Type: 1,
}
getStructTag(t)
}

输出结果
get tag is name
get tag is id
get tag is type

Post-body参数的类型

在POST请求中,常见的body数据类型包括

  1. application/x-www-form-urlencoded: 这是最常见的POST请求数据格式,适用于简单的表单数据。在这种格式下,数据以键值对的形式出现,每个键值对之间使用&符号分割,例如:key1=value1&key2=value2
  2. multipart/form-data:适用于上传文件或者二进制数据,通常用于文件上传功能。在这种格式下,数据被分割成多个部分,每个部分有自己的Content-Type,例如Content-Type:image/jpeg。这种格式下,数据以一定的边界符分割,每个部分之间以该边界符分割
  3. application/json:适用于发送JSON格式的数据,在这种格式下,数据以JSON格式组织,例如{“key1”:”value1”,”key2”:”value2”}
  4. text/xml: 适用于发送XML格式的数据,在这种格式下,数据以XML格式组织,例如value1

一般情况下,POST请求的body数据类型是需要根据API设计要求而选择的。如果混用不同类型的数据格式,服务器端可能无法正确解析请求的数据,导致请求失败。

ShouldBind方法

ShouldBind用于绑定请求中的参数,将其转换成Go结构体或者map类型,该方法的参数类型绑定顺序为

  1. 如果是query string,则按照form表单的格式进行解析
  2. 如果是post表单数据,则按照form表单的进行解析
  3. 如果是json格式,按照json格式解析
  4. 如果是xml格式,按照XML格式解析
  5. 如果是protobuf格式,按照protobuf格式解析

在绑定参数的时候,Gin会根据请求的Content-Type自动选择核实的参数绑定方式,可以通过ShouldBind方法来完成自动绑定。例如,如果请求的Content-Type为application/json,则Gin会自动将请求体中的JSON数据解析为Go结构体

为什么query string会按照form表单进行解析呢?form表单不是放在body里的吗?
虽然form表单数据通常被放在POST请求中的body里面,但是在HTTP请求中,form表单数据也可以以query string的形式出现在url中。在这种情况下,query string中的键值对与form表单中的键值对是相同的,都是由键和值组成的键值对,通过&符号进行分割
因此,Gin在解析query string时会按照form表单的格式进行解析,即将query string中的键值对解析为Go结构体或者Map类型。这样就能够通过Gin的ShouldBind方法统一处理query string和form表单数据,提高了代码复用性和可读性
需要注意的是,在将query string解析为Go结构体或者map类型的时候,需要将URL编码转义的字符进行解码。例如将%20转换为空格。Gin会自动进行这一步操作,不需要手动进行解码。

Gin中间件

Gin允许开发者在处理请求的过程中加上自己的钩子函数,这个钩子函数就被称为中间件,中间件适合处理一些公共的业务逻辑,比如登录认证、权限校验、数据分页、记录日志、耗时统计等等。
在JAVA等面向对象编程语言中,面向切面编程(AOP)的思想和中间件是类似的
而拦截器(interceptor)的思想和中间件也是类似的
AOP和MiddleWare、Interceptor都是用于改善软件系统架构的技术,但它们的实现和目标有所不同
相同点

  • 都是通过将特定功能从主要业务逻辑中分离出来,以改善系统的可维护性和可扩展性
  • 都是在系统中插入特定代码来实现所需功能的(hook)
  • 都可以提高代码的复用性,减少重复代码的编写

不同点

  • AOP关注的是切面,即与业务逻辑无关的横切关注点,如安全性、日志记录、性能检测等等,它们被成为切面,AOP使用依赖注入和动态代理等特定的技术,实现这些切面
  • 中间件关注的是不同系统组件之间的通信和交互,是一种软件层,为应用程序提供基础服务,如消息传递、数据传输和远程调用等等

AOP更关注于解决代码层面的问题,中间件则更关注于解决系统层面的问题

拦截器通常只在特定的代码路径或者逻辑流中执行,例如在特定的web请求或者调用特定的方法的时候,通常由程序本身实现,通过代码中的特定注解或配置来声明和使用,旨在通过拦截请求和响应来处理和修改它们,以实现特定的功能,如安全性、性能检测和日志记录等等
使用中间件

1
2
3
4
//注册全局中间件  
c.Use(middleware())
//注册某一条路由的中间件
r.GET("/xxx",MiddleWare(),handler)

*注意:在中间件或者handler中启用新的goroutine的时候,不能使用原始的上下文c gin.Context,必须使用其只读副本c.Copy()
c.Next和c.Abort

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
func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain {
finalSize := len(group.Handlers) + len(handlers)
if finalSize >= int(abortIndex) { // 这里有一个最大限制
panic("too many handlers")
}
mergedHandlers := make(HandlersChain, finalSize)
copy(mergedHandlers, group.Handlers)
copy(mergedHandlers[len(group.Handlers):], handlers)
return mergedHandlers
}
一个路由的中间件函数和处理函数结合到一起成为一条处理链条
本质上就是一个由HandlerFunc组成的切片


Next:
func (c *Context) Next() {
c.index++
for c.index < int8(len(c.handlers)) {
c.handlers[c.index](c)
c.index++
}
}
通过索引遍历HandlersChain链条,从而实现依次调用该路由的每一个函数

Abort:
func (c *Context) Abort() {
c.index = abortIndex // 直接将索引置为最大限制值,从而退出循环
}

Next:
image.png
Abort:
中断整个调用链条,从当前函数返回
我们的handlers也是HandleFunc类型,所以如果一条路由的专用middleware,调用了c.Next,其实就是直接跳到了handlers中去执行

优雅关机

优雅关机的使用场景,我们不能让一个项目随意的退出,因为这个时候可能还有请求没有处理完,如果你一个信号、或者一个stop按键能够直接让程序停止,那么显然这个项目是不合格的。正确做法是应该处理完所有请求、释放对应资源之后,再停止程序
下面是一个例子

1
2
3
4
5
6
7
8
9
10
// 把 run 放在子协程中执行
go func() {
r.Run()
}()
// 一个信号通道
exit:=make(chan os.Signal)
// 监听通道中有没有这两种信号
signal.Notify(exit,syscall.SIGINT,syscall.SIGTERM)
<-exit
log.Println("process exit")

为什么我会持续写面经?

从我去年投出第一份简历,经历第一场面试,到现在应该已经陆陆续续面了好几十场了,小厂、中厂、大厂都有,基本上每一场面试我都有做记录并且review,我觉得这是一件很有意义并且回报很高的事情

提升专业技能

我认为在技术岗方面,对于在校生来说,不论是实习还是秋招、春招,对于你的专业技能硬性要求大概只有这些

  • 工程方面的能力:你写了什么项目,你对自己的项目理解的程度深不深,常用的技术栈你学的怎么样,你的debug能力,把你招进来是不是真的能有产出,一些常用的框架、命令、工具你会不会用,会不会协同合作等等
  • 基础方面的能力:比如MySQL、Redis、MQ、Linux命令的使用,设计模式的理解,再到八股文(操作系统、计网、数据结构、计算机组成、编译原理、MySQL底层、Redis底层、MQ底层、一些工具的底层比如Docker等等)
  • 做题的能力:目前来看算法题能不能写出来越来越重要了,写不出题面试过的概率微乎其微
  • 知识的深度和广度:这些就属于随缘考察了,面试官问什么都可能,可能是智力题、逻辑题,也有可能是云原生、分布式、微服务这种比较新的东西

而做好面试记录,编写面经的过程中你就能很清楚的知道,在面试过程中你哪些点答的好,哪些点答的不好,你可以听面试官的语气来察觉出你给出的答案是不是他想听到的,然后对自己的整个知识框架进行查漏补缺

注意:这里的查漏补缺不是一有答不上来的问题就去学,这样成本太大了,挑你觉得你需要会的东西去学。

比如你从来没用过某个技术栈,所以答不上来某个问题,这个技术栈你目前用不到,也不是必会的技能,那就完全没必要去学。

但是如果你是某个很重要的点,比如设计模式不会,那我强烈建议你去好好学一学。

提升软素质

除了专业技能之外,面试也会考察候选人的软素质,比如语言表达能力、抗压能力等等。我觉得逻辑清晰,语句通顺,面试官能听懂你说的东西,并且觉得你说的有道理,这是很重要的一个能力。

比如说某个八股文的点,你和面试官的理解可能不一样,但是你能把自己的逻辑讲清楚,为什么会是这样,能够自圆其说,说出来的逻辑能够闭环,哪怕你说的东西是错的,都会比卡在那里好很多。

及时做面经能够很明显的感觉出来自己刚刚哪里表述有问题,哪里逻辑不清晰,哪里口误,哪里明明心里是会的,但是说出来很混乱,这样就能尽量避免下次再犯类似的错误。

正向反馈

不得不承认,真正热爱技术的人绝对是少之又少,大部分人投身技术领域主要还是为了一口饭吃。讨厌技术和热爱技术在学习和工作中绝对是两种不同的心态,并且成长速度也是绝对不一样的。

那么该怎么让自己不讨厌技术,甚至爱上技术呢?

引入开源思想,写面经、写博客,绝对是一个很好的方法。当我写出来的东西能帮他人解决问题,能够获得别人的赞同,能起到哪怕一点点微小的帮助,对我来说也是一种认同+满足,不断的重复这个过程,慢慢的去影响越来越多的人,帮助到越来越多的人,是一件很快乐的事情,至少对于我来说是这样。

养成review的习惯

及时复习,及时review绝对是帮助你提升效率的一大杀器,但是很遗憾的是大部分人都没有这个能力。从小我们接受的学习模式应该是:预习-正式学习-复习这么一个过程,上了大学之后基本没有预习这一说了,学习模式变成了:学习-复习这个过程。

不讨论这个模式是否正确,但是从记忆曲线来说,复习、及时复习、多次复习,这个过程的回报率是比只学习、不复习的模式强很多很多的,review一次的时间一般不会超过正式学习的20%,而效果会比只学习一次强很多很多。

所以不管是刷题、学技术、还是学知识,我都建议大家可以试着去review,如果不知道该如何踏出第一步,也许你可以试试从写一份面经开始。

limit与分页

在SQL中,limit用于限制返回的结果行数。LIMIT语句可以用于SELECT查询,用于限制查询结果集的行数,从而在处理大型数据集时,减少数据库的负载,提高查询的性能

基本语法如下

1
2
3
4
5
6
7
8
9
10
11
SELECT * FROM table_name LIMIT [offset],row_count;
//table_name是表名
//offset是可选的偏移量,用于指定要从结构集的哪个位置开始返回行
如果省略该参数,默认从第一行开始返回
//row_count一共返回的行数,也就是查询得到的数量

比如
select * from students limit 5,10;

或者
select * from students limit 10 offset 5;

limit在实际应用中常用于分页查询

举个例子

现在我有一个article表,想要做到文章分页展示的功能,每一页展示10篇文章

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//表结构如下

CREATE TABLE article (
id int(11) not null auto_increment,
title varchar(255) not null,
content text,
publish_time datetime not null,
primary key (id)
);

/这个时候调用方传来一个n,通常是Logic层往dao层传 伪代码如下
select * from article order by
publish_time desc limit ?,10 values (n*10);

//这条SQL就能做到文章分页的功能,按照时间来分页
//具体实践中可能没有这么简单,通常是热度、时间等等

深分页

查询结果集中的某个位置之后的记录,即查询结果集的偏移量很大的情况。这样需要扫描的数据量就很大,可能导致查询的性能变得很低下

如何避免深分页的问题

  • 使用更小的偏移量:比如将偏移量从10000降低到100
  • 使用分页键
  • 缓存结果集,在内存层面进行返回
  • 分库分表,减少每个表的数据量大小

分页键

分页键(pagination key)是一种用于分页查询的技术,它可以帮助我们在大数据集合中快速定位到需要查询的数据段。分页键通常是一个唯一的标识符,可以表示查询结果集中的某一行。在使用分页键的时候,通过查询分页键来定位结果集的起始位置,从而避免了偏移量很大的情况,也就是避免了SQL深分页的情况。

举个例子,假设我们需要查询一个包含一百万行数据的用户表,并且我们需要查询第500001到第500100行的数据。如果用偏移量的方式进行查询,需要查询前5000000行数据才能获得我们需要的结果,这将导致查询性能非常低下。而使用分页键的方式,可以在查询时直接指定分页键的值,从而定位到结果集的起始位置,避免了大量的数据扫描。

使用分页键的时候,我们需要选择一个合适的字段作为分页键,并确保该字段具有唯一性。通常情况下,自增长主键或者时间戳字段都是比较好的选择,分页键适用于有序数据集的分页查询

下面有一个具体的栗子

假设我们有一个包含大量文章的表,每篇文章都有一个唯一编号id和发布时间publish_time两个字段。我们需要查询发布时间在2022年1月1日到2022年3月31日之间的文章,并按照发布时间进行排序,每页显示十篇文章,显示第六页的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
1.选择分页键:根据查询条件,我们选择publish_time作为分页键

2.查询第五页的最后一篇文章的发布时间
我们得确定第五页最后一篇文章的发布时间
select publish from articles
where publish_time>='2022-01-01 00:00:00'
and publish_time<='2022-03-31 23:59:59'
order by pulish_time asc
limit 1 offset 50;

3.使用分页键查询数据
select * FROM articels
where publish_time>='分页键的值'
and publish_time<='2022-03-31 23:59:59'
order by publish_time asc
limit 10;

系统性能三指标

要想理解异地多活,我们需要从架构设计的原则说起

现如今,我们开发一个软件系统,对其要求越来越高,一个好的软件架构应该遵循以下3个原则:

  1. 高性能
  2. 高可用
  3. 易扩展
  • 高性能:系统拥有更大流量的处理能力,同时接口返回的速度应该尽可能的快。
  • 易扩展:系统在迭代新功能的时候,能够以最小的代价去扩展,系统遇到流量压力的时候,可以在不改动代码的前提下去扩容系统。
  • 高可用:通常由两个指标来衡量,分别是平均故障时间和故障恢复时间

不同的软件,不同阶段的公司,产品开发的不同阶段,对这些指标的要求是不一样的:比如一个初创公司,这个时候用户、流量最重要,开发功能,让页面尽可能美观,做产品比其他指标更重要;当流量上来了之后,怎么尽可能缩短响应时间,让系统能处理的QPS更大,就成了至关重要的问题;当产品使用的人数足够多,影响力足够大,可用性的重要就凸现出来了,怎么保证系统尽可能稳定,不出问题,对于公司来说才是最重要的。

系统发生故障是不可避免的,尤其是规模越大的系统,互相之间的调用也更加复杂,对于硬件的要求也越高,从理论上来说发生问题的概率也越大。这些故障一般提现在3个方面:

  1. 硬件故障:CPU 内存 磁盘 网卡 交换机 路由器
  2. 软件问题:代码BUG 版本迭代 线上故障等等
  3. 不可抗力:地震 水灾 火灾 战争

img

我们通常用N个9来表示系统的可用性高不高,从数据上能看出来,越到后面系统可用性带来的平均受益是越小的,但是难度是指数级别上升的。我们平常写的小玩具和企业级的应用差别也无非是这几个方面:高并发、高性能、高可用。

这些风险随时都有可能发生,所以在面对故障的时候,系统能否以【最快】的速度恢复,就成了可用性的关键。

如何做到快速恢复呢?

异地多活就是为了解决这个问题,而提出的高效解决方案

单机房

单机

img

客户端请求先进来,业务应用读写数据库,返回结果

这里的数据库是单机部署的,所以有一个致命的缺点:一旦遭遇意外,例如磁盘损坏、操作系统异常、误删数据,这意味着所有数据就全部【丢失】了,这个损失是巨大的

备份

我们可以对数据做备份,将数据库文件【定期】copy到另一台机器上,这样即使原机器丢失数据,依旧可以通过备份把数据【恢复】回来,以此保证数据安全

这个方案实施起来虽然比较简单,但存在两个问题

  1. 恢复需要时间:业务需要先停机,在恢复数据,这段时间服务是不可用的,对于一个系统来说这显然不能忍受
  2. 数据不完整:因为是定期备份,数据肯定不是最新的,会有丢数据的风险。这里补充一句,其实现代系统想要做到数据实时强一致性,几乎是不可能的

主从

img

可以在另一台机器上,再部署一个数据库实例,成为这个原实例的副本,让两者保持【实时同步】,这里的实时同步要打上引号,因为两台机器有网络隔离,永远不可能真正的实时同步,比如当主库写一条数据,还没同步的时候就宕机了,这个时候从库就会有丢数据的可能。

我们一般把原实例称为主库(master),新实例称作从库(slave),这个方案的优点在于

  • 数据完整性高:主从副本实时同步,数据差异很小
  • 抗故障能力提升:主库有任何异常,从库可以随时切换为主库,继续提供服务
  • 读性能提示:从库可以直接用来读

主从+多机器

同样的,业务应用也也可以在其他机器部署一份,避免单点。因为业务应用通常是【无状态】的,这里的无状态很好理解,业务代码的逻辑部分(除去数据库的部分),在什么机器上都能跑,且不会对机器造成持久化的影响,不像数据库一样存储数据,所以直接部署即可,非常简单。

img

因为业务应用部署了多个,所以现在还需要一个接入层,来做请求的负载均衡,一般是用nginx或者是lvs,这样当一台机器宕机之后,另一台机器也可以【接管】所有流量,持续提供服务。

img

提高可用性的核心思想

从这些方案可以看出,提升可用性的关键思路就是:冗余

担心一个实例故障,那就部署多个实例;担心一个机器宕机,那就部署多台机器;担心一个数据库可能会崩然后丢数据,那就多整几个数据库;这种冗余的思想放在机房层面,就产生了同城灾备、同城双活等方案;放在城市层面,就产生了两地三中心、异地双活、异地多活等方案

以上说的方案还是有缺点的,因为应用虽然部署了多台机器,但是这些机器的分布情况,我们并没有去深究。

而一个机房有很多服务器,这些服务器通常会分布在一个个【机柜】上,如果使用的机器刚好在一个机柜,还是存在风险。

如果恰好链接这个机柜的交换机/路由器发生故障,那么你的应用依旧有【不可用】的风险

哪怕是在不同机柜上,依旧会有风险,因为它们始终还是属于一个机房。

机房的故障率从现实角度来分析其实真的很低,建设一个机房的要求是很高的,地理位置、温湿度控制、备用电源等等。机房厂商会在各方面做好防护,但即使这样,还是有以下事故

  • 15年支付宝因为光纤被挖断,5小时无法访问支付宝
  • 21年b站服务器着火,3小时无法访问
  • 21年富途证券服务器断电,2小时无法访问

可见,哪怕机房级别的防护已经做的足够好,但只要有概率出现问题,那现实情况就有可能发生。虽然概率很小,但一旦发生,就会造成重大损失。

像前文所说的一样,不同体量的系统,关注的重点是不一样的。小系统关注的重点是用户,这个阶段用户的规模、增长就是一切。在用户体量上来之后,会重点关注性能,优化接口响应时间,接口打开速度等等。这个阶段更多的是关注用户体验,而体量再大下去,可用性就会变得尤为重要。像微信、支付宝这种全民级别的应用,如果机房发生一次故障,那么影响和损失都是巨大的

我们该如何应对机房级别的故障呢?没错,还是冗余

多机房

同城灾备

简单起见,可以在同一个城市再搭建一个机房,原机房为A,新机房为B,这两个机房的网络用一条【专线】连通。

img

为了避免A机房故障导致数据丢失,所以我们需要把数据在B机房也做【定时备份】。这种方案,我们成为【冷备】。因为B机房只做备份,不提供服务,只有在A机房故障的时候才会弃用。

或者可以把AB之间的关系换成主从的关系,这样不仅能提高系统吞吐量,也能够更加保证数据的完整性

img

在这个方案的设想中,如果A机房真挂掉了,要想保证服务不中断,还需要做这些事情

  1. B机房所有从库升级成主库
  2. 在B机房部署应用,启动服务
  3. 部署接入层,配置转发规则
  4. DNS指向B机房接入层,接入流量,业务恢复

整个过程的每一步需要人为介入,且需要花费大量时间,回复之前整个服务还是不可用的,如果想要做到故障之后立即【切换】,就需要考虑下面这种架构

img

这样的话,A机房整个挂掉,我们只需要做两件事

  1. B机房所有从库提升为主库
  2. DNS指向B机房接入层,接入流量,业务回复

这种方案我们叫【热备】,热备相比于冷备的最大优点是随时可切换,不同点有需要多加一层应用层和接入层,同时数据库层面的定时备份变成了实时备份,这些都是需要额外开销的。我们把这两个方案统称为:同城灾备

同城灾备的最大优势在于,我们不用担心【机房】级别的故障了,一个机房发生风险,我们只需要把流量切换到另一个机房,当然这不一定会没有问题,比如冷备的问题是之前的备用系统没有经过流量的测试,不一定能扛得住;热备也是,瘫了一个主系统,那么备用系统的压力范围,也不一定能抗住。

同城双活

虽然有了应对机房故障的解决方案,但是有个问题是不能忽略的:A机房挂掉,全部流量切到B机房,B机房是否真的能如我们所愿,正常提供服务?

另外从成本的角度上看,我们新部署一个机房,需要购买很多硬件资源,花费成本也是非常高昂的,如果只是放在那里不去使用,是很浪费资源的一种表现。

因此我们需要让B机房也接入流量,实时提供服务

只需要把B机房的接入层IP地址,加入到dns服务中,这样B机房从上层就可以有流量进来了

img

这里有一个新问题:B机房的存储都是从库,而从库默认都是不可写的,也就是说B机房是处理不了写请求的。这个问题就应该在业务应用层解决,需要区分读写分离,一般是通过中间件实现,写流量给A机房,读流量可以给两个机房

这种架构有什么问题呢?

两地三中心

因为把两个机房当成一个整体来规划,如果是一个城市的话,当整个城市发生自然灾害的时候,例如地震、水灾,那么依旧可能有【全局覆没】的风险

这个时候就可以将备份机房放在另一个城市

两地三中心就是指两个城市,三个机房,其中2个机房在同一个城市,并且同时提供服务,第三个机房部署在异地,制作数据灾备。

这种架构方案,通常用在银行、金融、政企相关的项目中,问题还是启用后的服务,不确定能否如期工作。

所以想要真正抵御城市级别的故障,越来越多的互联网公司,开始实施【异地双活】

异地双活

主要问题是跨机房的延迟调用,当B地的应用去跨区域读写A地的存储,网络延迟就会让整个请求变得非常慢。而要解决这个问题,就必须在存储层做改造了。

B机房的存储不再是从库,而也要变为主库,同时两个机房的数据还要【互相同步】,无论客户端写哪一个机房,都要把数据同步到另一个机房。因为只有两个机房都拥有全量数据,才能支持任意切换机房,持续提供服务。MySQL本身是提供了双主架构的,支持双向数据复制,但平时用的不多。而且Redis、mongoDB等数据库是没有这个功能的,所以必须开发对应的【数据同步中间件】来实现双向同步的功能。

除了数据库这种有状态的软件之外,通常还会用到消息队列,例如rabbitMQ,kafka等,这些也是有状态的服务,所以它们也需要开发双向同步的中间件,支持任意机房写入数据,同步至另一个机房

业界开源出了很多数据同步中间件,例如阿里的canal、redisshake、mongoshake,可分别在两个机房同步MySQL、REDIS、MONGODB数据

这样的话有一个新的问题,两个机房都可以写,如果操作的是同一条数据,就很容易发生竞态的问题

分别有两个方案

  1. 消息同步中间件要有自动解决数据的能力,区分出操作的先后顺序
  2. 从源头避免数据冲突的发生

一般都是采用第二种方案:在最上层接入流量的时候,就不要让冲突的情况发生。

具体来讲就是将用户区分开,部分用户请求固定达到北京机房,其他用户请求固定打到上海机房。进入某个机房的用户请求,之后的所有业务操作,都在这一个机房内完成,从根源上避免【跨机房】。

这时候需要在接入层之上,再部署一个路由层,自己配置路由规则,把用户分流到不同的机房内。

一般来说有三种方式

  • 按业务类型分片,比如某个子域的请求固定全打在某个机房
  • 直接哈希分片,先对请求进行哈希,再对机房的数量进行取模,这样可以保证流量均匀分布到某个机房,但是对于某些请求来说可能速度会慢,比如一个新疆的请求,打到了广州机房,网络延迟就会比打在西安机房大
  • 按地理位置分片,请求只会打在距离自己最近的机房,处理请求的速度快,但是流量不均匀

异地多活

把异地双活的思想推到多个城市,部署多个机房

前言

我是一个很喜欢用文字记录生活与想法的人,但是由于文字功底太弱,这些文字大多都躺在我的备忘录和废纸篓里。
昨天看到了一个我很尊敬的学长写的一篇博文,深有所感。于是想写一些东西,记录一下大学这两年的一些经历,感悟和想法。部分内容摘自实习求职总结

我的本科前半生

大一

20年9月16日,我独自坐上了从景德镇到南昌的动车,在体验过人生的第一次地铁和第一次飞机之后,在晚上十一点抵达了重庆。当时订的酒店在机场附近有一个总店一个分店,很不幸,我订的是总店但是不小心跑到分店去了。当时下着淅淅沥沥的小雨,我背着我的电脑包和双肩包走在凌晨的大街上,忽然觉得有些冷,那晚我把QQ签名改成了“希望生活别把我揍的太惨”。
image.png
进入大学后,我并没有选择松懈。我觉得人就是这样,越长大要承受的东西只会越来越多,万万没有变轻松的道理,中学比小学难熬,大学比中学难熬,进了社会比在学校难熬。我选择了在报道的第二天就一头扎进了图书馆,并在一个礼拜内速通了《高等数学上》及《线性代数》的大部分内容。开学之后没多久就放国庆了,但是国庆我也没有闲着,在之后的几个礼拜我又陆陆续续的把《C语言程序设计》学完了,当时真的非常幼稚,一有问题就在群里问,一有看不懂的代码就直接打包让当时的C语言志愿者帮忙看,完全不知道这是一种多么不负责任的行为,现在想想真的是愧怍万分。
当时其实完全没有思考过自己到底想在大学里面干什么事情,在毕业的时候成为什么样的人,只顾着努力学东西。我只是把高中的那一套东西复用在大学里了,打算成为班里面的尖子,然后争取保研,但是我真的需要一个硕士学位吗?
这何尝不是一种懒惰呢?还没有确定明确的目标就慌不择路的随便选条路出发,现在回头看看只觉这是在用战术上的勤奋来掩盖战略上的懒惰罢了。
大概是十一月初,我大学生活最重要的一个转折点来了。我去参加了当时我们学院的优秀就业生宣讲,第一个宣讲人是胡仓学长,也是一个很厉害很厉害的学长,在听完他的经历之后我花了一下午仔细思考了之后的规划,最终决定不读研了,本科毕业直接就业。这个决定其实并没有得到家人和朋友的支持,但我并没有动摇我的想法,人生总得自己去体验嘛。
image.png
之后我就很少去上课了,因为我觉得那些课程真的很浪费时间,大部分课程都是通识课,对就业可以说是毫无帮助,开设的少部分专业课,老师只是在念年纪比我都大的PPT,一节课一个半小时,我想如果自学的话也许花不了二十分钟就能把一节课的内容给学完。
大一也没有确定好方向,同时也缺乏学习的方法论,基本上ACM、CTF、安全、后端、前端,这些方向我都尝试过,当时很多开发环境我都装不上,经常是一个环境装好几个小时,最后还是不了了之;除此之外刷了一些算法题也并没有体现出来效果。这对一个刚接触技术的小白来说打击太大了。加上当时其实对写代码根本说不上有什么兴趣,我很快就慢慢对写代码这件事情丧失了动力,但我又决定了本科毕业就业,于是乎我开始选择逃避现实了,几乎每天都窝在寝室里,不去上课也不做正事,每天看看小说刷视频打游戏。一边因为迷茫而焦虑,同时又缺乏自驱力和行动力,无法走出自己的舒适圈,我想这是很多大学生的问题。

大二

我是一个很难因为自己而开心起来的人,我直到现在都没有找到真正能让我很开心的事情,我总觉得我存在的意义是为了周围的人,为了我的家人、伴侣和朋友。如果说取悦自己是一门课程的话,那我一定没有及格。
其实到现在我都不能理解为什么有人的开心能来的这么容易,我的好哥们会因为吃到好吃的而觉得开心,他可以为了吃专门跑出去一趟,我长这么大似乎都没有很喜欢很喜欢吃的东西,大部分食物都只是用来饱腹而已。
这可能也和多巴胺能有关系,有的人就是很容易觉得幸福和满足,不需要出人头地,平平淡淡的生活已经能够让他们很开心很开心了。而有的人多巴胺能很强,对自己的现状很容易产生一种不满足的心理,这种人更容易取得一点成就,但是这一定就是好事吗?我并不觉得这种世俗意义上的成功和幸福感是有正相关性的。之前我一直希望我家的小孩能够好好读书,出人头地,大有作为;但现在我完全不这么想了,现在的小孩能活的开心的太少了,不需要有什么成就,开开心心的过一生就很好。
如何经营好自己的生活、享受当下是我觉得我急需学习的一项技能。好消息是现在我已经迈出了第一步,我开始学画画、摄影、骑行、读书、分享自己的生活。
AB205C830C36C9F4AB42E05364A29BBA.jpg
就像中学一样,我是因为不想让家人过于担心我的成绩才开始努力学习,最终完成逆袭的。在大二开始之前的那个暑假我认识了当时的爱人,因为恋爱的原因,我觉得自己不能再继续颓废下去了,当时刚好极客勤奋蜂工作室开启了Go学员的培训,为了通过工作室的考核,我开始把大一学过的东西慢慢捡起来,然后每天把大量的时间丢在写代码,学技术上面。不知道是不是我的性格如此,不管是什么东西,只要我花了足够多的时间,我就会慢慢的不排斥这件事甚至喜欢上它。每做完一个嘉文学长设置的project我都会更热爱代码一分,每划掉每日to-do list中的一项我的成就感就会多一分,在成就感和满足感积累之中,我慢慢的开始喜欢上技术了。我想也许就是这样吧,在大学我也读过几本心理学的书籍,满足感、成就感、被需要感是做某些事情的最佳动力了,同时,一个最可怜的人也一定不是缺少财富、权利、地位,而一定是不被需要。
image.png

img

在21年年底的时候,我在QQ空间里面看到了有学长分享食铁兽招新的信息,然后加入了招新QQ群,22年1月13日,那时候我刚考完《数据结构》这门课,这也是那学期的最后一门考试,考完之后突如其来的约面了,那是我人生中的第一次技术面试,说实话答得很差,完全没有准备过八股,只能凭借平常做项目的一些实践经历答上来一部分问题,大概只答上来了一小半吧,因为是第一次面试,我的印象真的非常深刻,问的问题有Redis的持久化,Redis的数据结构,HTTP2的特性这些,面试官还和我讨论了一下Go的优劣和以后职业规划的事情。
之后食铁兽的负责人晚上又和我联系了一下,简单问了一些职业发展之类的问题。第二天通知我面试通过了,当时真的非常非常开心,第一次觉得自己在技术上或者说是在专业知识上被认可了,然后当天就去了食铁兽那边,见到了面试官源哥,婧姐,还有负责人峰哥。当天主要和峰哥、源哥聊的比较多,这也是我第一次了解创业相关的东西,比如融资是怎么一回事。
之后的话就放寒假了,我当时在家不怎么学习,基本上都是食铁兽给的一些任务PUSH着我去学,比如尝试基于TCP、UDP、KCP、和HTTP编程,还有当时Go的最新版本是1.18beta,推出了泛型这一特性,但是还没有来得及对内置数据结构(slice map等)进行泛型的封装,于是我去学习了泛型,实现了简单的泛型封装。
差不多三月到五月吧,基本每个周末都会去食铁兽那边敲代码,那边的环境是真的很好,呆着就容易让人心情愉悦,我慢慢的把分布式消息队列的拼图一块块补全,从raft选主的实现,到消息的存储和删除,到消息的切片,再到考虑结点状态进行消息的分发,慢慢的也把这个项目做出来了,这也是我第一个不看视频写出来的非web项目。
56E6CD019183D099581FB095CF187A7C.jpg
源哥曾经说过这么一段话,我深以为然。
其实编程真的不是一件很难的事情,甚至是一个比较有意思的事情。只是大部分人还在学的阶段就放弃了,这个阶段他们只能抄别人的代码,自己写不出来东西,一旦自己能创造东西就能感受到编程的快乐了。
除了工程上的收获之外,我还认识了很多很厉害的大佬,基本上食铁兽成员的专业能力已经是我校一级梯队水准了,比如源哥,给我的感觉就是在技术上钻的很深,不管是理论知识还是工程能力都很强,架构层面的知识也很优秀,之前问过他一点设计方面的题目,能感觉出来他思考的方面特别多。而且他知识的广度也很强,现在我也大三下了,感觉我也很难在一年之后达到这样的水平。还有峰哥、翔哥、婧姐、洋哥,也都是很优秀的人,就不一一说了,食铁兽的氛围是真的挺好的,大家都是学生,没有那种领导的感觉,这些哥哥姐姐们也很照顾我,在那边呆着也很舒服。
差不多五月开始正式准备八股和算法了,那段时间的学习强度真的特别大,也是我在校园最痛苦的一段时间,差不多白天一整天都在工作室呆着,学到脑子疼才结束,晚上睡眠质量也差,需要吃褪黑素才睡得着,不然脑子会一直处于活跃状态,明明身体很困,但是意识非常非常清晰。差不多准备了两个礼拜之后就开始投递简历了,投了很多厂子,不过大部分厂子看到是24届就直接拒了,当时真的非常焦虑,觉得自己是不是要找不到工作了。最后好像只有几个小厂还有字节给了面试机会,字节的算法没做出来,于是挂掉了,最后拿了一个小厂的offer但是不太想去。
没有找到合适的实习就选择了去老师的实验室干活,做的是偏运维的华为HPC高性能迁移项目,其实就是写脚本,不过真的挺折磨的,需要保证一个大的脚本一次性执行成功,那段时间我对于linux和shell的熟悉程度也在直线上升。同时也和我校的研究生学长进行了交流和接触,更坚定了我不读研的想法。

“学长你学的什么方向啊?”
“开发吧,JAVA学了一半,没完全学完”
“那你发了paper吗?”
“还没有,还在写”
“打算毕业之后就业还是接着深造啊?”
“就业,读不下去了,读研都已经觉得很痛苦了。”
“读研究生有补贴吗?”
“一个月800”

大三

在大三入学后我又开始了新一轮的投递和面试,在开学的一周内拿了两家公司的offer,最后选择了去通明智云实习,这是一家搞负载均衡的公司,我们组负责开发负载均衡设备的纳管平台。
那是第一次出远门实习,在此要感谢我的辅导员——林义钧老师,他是一位真正有师德,替学生考虑的老师,替我解决了学校里的很多麻烦,在大部分院都卡学生外出实习、完全不考虑当今就业市场学生可能毕业即失业、只顾学生就业率、升学率的情况下,林义钧老师仍然支持我实习,并替我解决了请假审批的问题。
在通明智云我学到了很多东西,这是我第一次接触到生产级别的项目。我的leader韩哥是一个非常好的领导,并没有因为我是实习生就给我分配边角任务,而是真正有让我积极参与项目、融入到整个集体里面来,同时他也会让每个人不只负责一个模块,而是定期更换模块,让所有人对项目都有多方位的理解,也能真的学到技术。
689294415A37CFD7521DF177B8FA68A9.jpg
一开始主要负责部署相关的工作,负责和用户对接,能被leader信任,真的去做事情对我来说真的是一件挺有意义的事情。后面主要负责Prometheus相关的开发工作,但是没深入多久就和我当时的爱人分手了,后面整个人的状态其实都不太好,也没有多少产出,也确实是一件挺遗憾的事情。
公司里还有来自北邮、北林的实习生,该说不说,长得都很帅。在之前我没觉得重邮本学历有多低,在实习之后发现好像确实有点低了。大家一起玩ranging loop、爬长城是真的挺开心的,长这么大第一次碰到除了LOL之外戳我的游戏,我之前一直以为自己不爱玩游戏呢。
组里面的同事也很友好:说话很好笑很热心的美美、对技术很有追求的建国、像大哥哥一样很温暖的恩清、很腼腆的PM若培、喜欢玩摩托的士亮等等。还记得我理解不了需求、写不来mongo的查询语句、搞不懂prometheus实现原理、部署出问题的时候基本都是问的美美、建国和恩清,谢谢他们愿意花时间替我来擦屁股。总的来说大家都很好,氛围很温暖,下了班大家也会去地铁口那边一起吃饭,平常住在一栋楼里面交流也多。真的是一段很难忘也很有意义的经历,不仅仅是技术方面成长了,连带着待人接物、生活技能方面也学会了不少东西。
现在我加入了百度APP业务中台部门,虽然呆的时间还不久,但是也能够感觉出来氛围挺好的,我的mentor宇哥和leader学明哥相处起来都给人一种很舒服的感觉,并且也是真的希望我能够学到东西,在我反应了我更想往Go方面去靠的时候,他们立马给我确定了之后的任务方向。百度给我的感觉是新人培养制度真的很不错,有很详细的培养方案,能够帮助实习生快速融入团队,同时整个部门的效率也非常高,不会在不该浪费时间的地方浪费时间,组里还有来自北大硕、北航硕的实习生,真的都是一群很优秀很优秀的人。
9CE5E3FDD31967D5D0140BB2F68751A9.jpg

总结

感觉自己真的很幸运,这么多年都没有碰到过坏人。从入学认识的室友、导员,到后面认识的学长学姐:胡仓学长、宋扬学长、彩嵘学长、文炀学长、嘉文学长,再到食铁兽认识的小伙伴:源哥、峰哥、婧姐、洋哥,再到通明智云的同事和leader,还有现在的mentor、同事和leader,大家都是很好很好的人,相处起来都非常舒服,感谢所有遇见。
大三还没结束,要做的事情还有很多。回头看看其实比起刚入学那一会,我已经成长不少了,也学会了一些道理。也培养了一些比较好的习惯,比如我上大学前完全想不到我现在在假期也会学习、会定期阅读和运动、会主动走出舒适区去学一些我觉得我应该会的技能。

当然大学里也有一些不好的经历,甚至有些事情我到现在也没有想明白原因,不明白自己为什么要被这么对待。虽然不理解,但是也只能试着接受。

希望我能过好大学的最后一年零几个月的生活,收拾行囊,再度出发吧。也希望我能一直热爱编程,热爱我的事业。我几乎不在社交媒体上发编程相关的东西,一是觉得我还是个noob,需要学习的东西还有很多;二是不爱给自己打上程序员的这个tag,不想给自己设限。