分类 杂记 下的文章

掏出了吃灰的树莓派打算自己搭建一套 DevOPS,树莓派在 hosts 中增加了域名解析。

# pi
192.168.124.15 pi.local

搭建好 nginx 后,那个不安全的字样怎么看都难受,有点强迫症,在线申请免费的 SSQL 有点麻烦,所以就找到了 mkcert。

$ brew install mkcert

安装本地 CA 证书

# 这个步骤需要管理员权限
$ mkcert -install

生成证书:

mkcert  "pi.local"

最后传到树莓派上,设置 nginx。

2023-04-07T12:45:29.png

喜大普奔,大功告成。

参考:

https://www.cnblogs.com/sleepyocean/p/16159327.html
https://juejin.cn/post/7034902197020131364

2023-04-03T07:45:34.png

最近失业在家待业,思考了以前的架构设计以及代码设计中的问题。

一次有趣的面试经历

去年在杭州面试一家只有五个程序员的小公司,后台开发人员只有一个人,业务其实并不复杂,其后端采用了微服务架构,于是和对方“犟” 了起来,后来反思这是没有必要的,没有必要和对方犟。

因为对方的老板也在场,其实那个时候我”落“了对方的面子,所以面试结果可想而知,并没有通过,大家一定要引以为戒。

不过,直至今日,我依旧认为对方的技术选型是错误的,是典型的为了用而用,没有去考虑企业的当前场景。架构设计是为业务服务的,最主要的应该是考虑当前的应用场景、当前公司目标追求。

当我踏入那家公司的时候,我看到了白板上的各种微服务划分,DTM 分布式事务管理,我整个人是蒙的,只有一个后台开发,还找么搞,而且还是个初创企业,这样会浪费大量的时间去处理各种由于微服务带来的问题,这对初创企业的投入产出是非常低的,这个阶段的企业考虑的应该是如何活下来,而不应该将大量的事件浪费在技术选型上,以及去不断地解决由于系统复杂度过高导致的各种问题。

2023-04-03T07:51:04.png

如果有微服务开发经验的小伙伴,应该知道,微服务的 Debug 要比单体应用难上很多,而且也会提高新人入职入手工作的难度,毕竟那么多复杂的调用关系,如果文档不够齐全的话,那基本上都是想要一头撞死。

我们真的需要微服务吗?

我认为需要看阶段,在创业初期是不合适的,因为技术是为了业务服务的,如果这个时候就一头扎入到所谓的微服务中,那么将浪费大量的精力,而且微服务也不是”银弹“,它甚至会引发各种各样的问题。

首先说一下微服务的作用吧,微服务简单的理解其实就是分而治之,将复杂的单体应用拆分成一个个独立的微服务,这样就可以降低代码的复杂度,同时为了配合微服务的开发,我们还需要对组织架构进行调整,以配合我们的架构设计。

在励步时候,励步启蒙APP 的后端采用的就是微服务架构,由于当时的团队解散,这个项目到了我们团队手中,我认为当时的架构主要存在下面的问题:

  1. 技术选型混乱,有的地方采用 Gin,有的用 beego,有的地方用 Irsi,有的地方用 ZORM 有的用 GROM,这就导致学习成本激增
  2. 服务之间通信混乱 RestAPI,GRPC,HTTP 调用都有。
  3. 没有配置中心,没有服务发现,配置和服务发现都是在配置文件中写死的,好在一共就三台机器。

当时给我的感觉这要是个单体应用该多好,每一次迭代的时候涉及到多个微服务,我都想撞墙,因为每个服务采用的技术选型都不一样,还需要去看好几个文档。

再有就是我在帷幄匠心的两个月时间,当时的架构也是采用微服务设计,团队成员的技术能力其实非常强,我们有专门的基础架构部门去为业务团队开发各种底层工具,但是当时的微服务治理做的并不是很好,每一次大规模迭代的时候,等 Jenkins 排队都需要好久,同时测试环境冲突特别多,当一个服务同时有多个人员进行迭代的时候,就会发现还需要再测试环境上进行排队,因为混存在代码冲突,服务冲突等抢矿,导致研发效能降低了不少。

这其中出现的问题,我认为最根本的原因是因为组织架构没有做好配合,我们团队一共十几个人,所有人穿插进行开发,没有人固定的负责某个业务模块,这就导致为了同时并行开发多个需求,多个人都要去修改一个微服务,这也就就导致各种环境冲突的原因之一。

那什么时候用比较合适的呢?

我认为在业务已经趋于稳定,原来的代码已经成为了历史遗留系统,甚至成为团队的负担的时候就是微服务化的时候了,使用”绞杀式“的架构方案,逐步对老的功能进行替代。

单体应用的性能问题

有很多人可能会觉着,单体应用性能差,当我们达到一定的量级,一定会是会走向分布式,走向微服务,其实原来我也是这么认为的,直到我看到了下面这篇文章。

首先是微服务,我们真的需要微服务吗?

《没用微服务,Shopify的单体程序居然支撑了127万/秒的请求?》

阅读地址:https://colobu.com/2022/12/04/Shopify-monolith-served-1-27-Million-requests-per-second-during-Black-Friday/

更新日志

  • 2023-03-22 部署方式更改成了 docker-compose

到目前为止,我已经记不清这是第几次折腾博客了,我希望这是最后一次。

这两年一直在用的是 hugo ,其实还是很不错的,生成 html 后用 shell 脚本直接就可以发布,但是由于自己比较懒,一直没有去搞评论模块,因为觉着感觉很麻烦,毕竟博客系统千千万,实在不行咱就换。

当前博客的架构

当前博客架构.png

HTTP 服务器选择使用 Caddy ,这是因为之前使用 hexo 的时候就选用了 Caddy 作为 HTTP 服务器,主要是使用他可以减少更多的手动配置,比如自动生成 SSL 证书,这样就非常方便,要不然还得去申请证书,部署证书,麻烦死了,同时 Caddy 的配置文件相对于 Nginx 而言非常简单,看个半个小时基本上就可以上手了。

因为是个人博客完全可以不考虑性能问题,Nginx 和 Caddy 的性能差异其实可以完全不考虑,怎么方便怎么来,没有安全漏洞就可以。

以下是配置文件:

xxx.xxx.xx {
  log {
        output file /data/logs/www/www.maksim.website.log
  }
  tls [email protected]
  reverse_proxy /* localhost:8999 {
    header_up X-Forwarded-Host {host}
  }
}

短短几行的配置就已经搞定了,是不是很简单。

选择的 Docker Image。

因为随着 Docker 在工作中的使用越来越多,习惯之后简直就是懒人福音,再也不用去下载各种安装包,然后再各种编译安装了,使用 Docker 压缩了大量的时间,让我可以有时间干其他事情,我目前的机器配置很垃圾,编译 PHP7 的话估计至少需要 10 分钟左右,这还没算中间可能存在各种各样的问题,人生苦短,及时用 Docker,而且日后迁移也非常方便。

docker image 选择了官方封装的镜像,地址如下:

typecho/Dockerfile: Docker Image packaging for Typecho (github.com)

安装命令:

docker run --name typecho-server -v  /var/typecho:/app/usr  \  
-p 8999:8080
-e TYPECHO_SITE_URL=https://your-domain.com \
-d joyqi/typecho:nightly-php7.4 

在安装过程中我使用的 Sqlite ,因为一个博客系统流量其实并不大,再使用 MySQL 就有些重了,而且还增加了使用成本,我的 VPS 配置很低,再跑一个 MySQL 真的是要了老命。

[root@vultrguest ~]# free -h
              total        used        free      shared  buff/cache   available
Mem:          821Mi       281Mi        77Mi        16Mi       461Mi       388Mi
Swap:         2.3Gi       120Mi       2.2Gi

现在内存就已经快耗尽了,已经开启了 Swap。

注意:在选择 Sqlite 的时候,可能有些 Bug,跑在 docker 里的 typecho 不会给自动生成 db 文件,所以需要在外面使用 sqlite3 xxxx.db 了,来生成 db 文件,并且将 db 文件的上层目录的文件权限设置成 777,否则 typecho 将无法操作 typecho。

这样一来 typecho 就已经搭建好了,过段时间其实还是要迁移到我的 k8s 集群上面。

迁移到了亚马逊

亚马逊一直有优惠活动,可以说很给力,1 年的免费期,所以就把博客迁移到了亚马逊上。

不过这次使用了 docker-compose,之前的 caddy 是直接启动的进程,这次为了方便,同时也是为了以后迁移方便,所以选择了 docker-compose,这样在做迁移工作就省时省力了。

目录结构如下:

drwxr-xr-x 4 root root  48 3月  20 14:09 caddy
-rw-r--r-- 1 root root 933 3月  20 14:01 docker-compose.yaml
drwxrwxrwx 6   33 tape 111 3月  22 11:17 typecho

docker-compose.yaml 如下:

version: '3'

services:
  caddy:
    image: caddy
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - $PWD/caddy/Caddyfile:/etc/caddy/Caddyfile
      - $PWD/caddy/data:/data
    restart: always
    environment:
      - TZ=Asia/Shanghai
    networks:
      - frontend

  typecho:
    image: joyqi/typecho:nightly-php7.4-apache
    volumes:
      - $PWD/typecho:/app/usr
    restart: always
    expose:
      - "80"
    environment:
      - TZ=Asia/Shanghai
      - TYPECHO_DB_ADAPTER=Pdo_SQLite
      - TYPECHO_SITE_URL=https://www.maksim.website
    depends_on:
      - caddy
    networks:
      - frontend
networks:
  frontend:

一些问题

  1. 由于使用的是 docker 创建的项目,一些插件和模板在安装的时候,需要依靠挂在的目录,这个时候需要注意文件权限。
  2. 没有找到日志记录在哪里,需要在看一下。

这篇文章来源于我在团队内部分享的内容,PPT 和代码已经上传到了 github。

better-maksim/swoole-rate-limiting: 基于 swoole 实现限流算法 (github.com)

  • 什么是限流
  • 限流的实现原理
  • 优缺点分析

什么是限流?

  • 通过并发限速(拒绝[降级]、排队、等待)形式达到保护服务的目的。
  • 一部分人能访问,好过所有人都无法访问。
  • 如何做:统计当前并发情况,当并发量达到预设峰值时拒绝访问。

生活中的限流场景

  1. 布达拉宫需要提前几个月进行预订
  2. 海底捞在满座后需要进行等待
  3. 魔兽世界需要排队登录
  4. 12306 如果使用刷票软件的话会被暂时禁止访问,还有就是限制时段购票

常见的限流算法

  • 计数器
  • 滑动窗口
  • 漏铜
  • 令牌

计数器算法

计数器固定窗口算法是最基础也是最简单的一种限流算法。原理就是对一段固定时间窗口内的请求进行计数,如果请求数超过了阈值,则舍弃该请求;如果没有达到设定的阈值,则接受该请求,且计数加1。当时间窗口结束时,重置计数器为0。

2022-09-29T16:27:51.png

计数器固定窗口算法原理图

特点分析

优点:实现简单,容易理解。

缺点:流量曲线可能不够平滑,有“突刺现象”,如下图所示。这样会有两个问题:

2022-09-29T16:28:43.png

计数器固定窗口算法限流曲线

一段时间内(不超过时间窗口)系统服务不可用。比如窗口大小为1s,限流大小为100,然后恰好在某个窗口的第1ms来了100个请求,然后第2ms-999ms的请求就都会被拒绝,这段时间用户会感觉系统服务不可用。

窗口切换时可能会产生两倍于阈值流量的请求。比如窗口大小为1s,限流大小为100,然后恰好在某个窗口的第999ms来了100个请求,窗口前期没有请求,所以这100个请求都会通过。再恰好,下一个窗口的第1ms有来了100个请求,也全部通过了,那也就是在2ms之内通过了200个请求,而我们设定的阈值是100,通过的请求达到了阈值的两倍。

2022-09-29T16:29:58.png

计数器固定窗口限流算法产生两倍于阈值流量的请求

计数器滑动窗口算法

计数器滑动窗口算法是计数器固定窗口算法的改进,解决了固定窗口切换时可能会产生两倍于阈值流量请求的缺点。

滑动窗口算法在固定窗口的基础上,将一个计时窗口分成了若干个小窗口,然后每个小窗口维护一个独立的计数器。当请求的时间大于当前窗口的最大时间时,则将计时窗口向前平移一个小窗口。平移时,将第一个小窗口的数据丢弃,然后将第二个小窗口设置为第一个小窗口,同时在最后面新增一个小窗口,将新的请求放在新增的小窗口中。同时要保证整个窗口中所有小窗口的请求数目之后不能超过设定的阈值。

2022-09-29T16:31:59.png

从图中不难看出,滑动窗口算法就是固定窗口的升级版。将计时窗口划分成一个小窗口,滑动窗口算法就退化成了固定窗口算法。而滑动窗口算法其实就是对请求数进行了更细粒度的限流,窗口划分的越多,则限流越精准。

特点分析

  1. 避免了计数器固定窗口算法固定窗口切换时可能会产生两倍于阈值流量请求的问题;
  2. 和漏斗算法相比,新来的请求也能够被处理到,避免了漏斗算法的饥饿问题。

漏斗算法

漏斗算法的原理也很容易理解。请求来了之后会首先进到漏斗里,然后漏斗以恒定的速率将请求流出进行处理,从而起到平滑流量的作用。当请求的流量过大时,漏斗达到最大容量时会溢出,此时请求被丢弃。从系统的角度来看,我们不知道什么时候会有请求来,也不知道请求会以多大的速率来,这就给系统的安全性埋下了隐患。但是如果加了一层漏斗算法限流之后,就能够保证请求以恒定的速率流出。在系统看来,请求永远是以平滑的传输速率过来,从而起到了保护系统的作用。

2022-09-29T16:32:53.png

漏斗算法原理图

令牌桶算法是对漏斗算法的一种改进,除了能够起到限流的作用外,还允许一定程度的流量突发。在令牌桶算法中,存在一个令牌桶,算法中存在一种机制以恒定的速率向令牌桶中放入令牌。令牌桶也有一定的容量,如果满了令牌就无法放进去了。当请求来时,会首先到令牌桶中去拿令牌,如果拿到了令牌,则该请求会被处理,并消耗掉拿到的令牌;如果令牌桶为空,则该请求会被丢弃。

特点分析

  1. 漏桶的漏出速率是固定的,可以起到整流的作用。即虽然请求的流量可能具有随机性,忽大忽小,但是经过漏斗算法之后,变成了有固定速率的稳定流量,从而对下游的系统起到保护作用。
  2. 不能解决流量突发的问题。还是拿刚刚测试的例子,我们设定的漏斗速率是2个/秒,然后突然来了10个请求,受限于漏斗的容量,只有5个请求被接受,另外5个被拒绝。你可能会说,漏斗速率是2个/秒,然后瞬间接受了5个请求,这不就解决了流量突发的问题吗?不,这5个请求只是被接受了,但是没有马上被处理,处理的速度仍然是我们设定的2个/秒,所以没有解决流量突发的问题。而接下来我们要谈的令牌桶算法能够在一定程度上解决流量突发的问题,读者可以对比一下。

令牌桶算法

2022-09-29T16:34:17.png

令牌桶算法是对漏斗算法的一种改进,除了能够起到限流的作用外,还允许一定程度的流量突发。在令牌桶算法中,存在一个令牌桶,算法中存在一种机制以恒定的速率向令牌桶中放入令牌。令牌桶也有一定的容量,如果满了令牌就无法放进去了。当请求来时,会首先到令牌桶中去拿令牌,如果拿到了令牌,则该请求会被处理,并消耗掉拿到的令牌;如果令牌桶为空,则该请求会被丢弃。

特点分析

令牌桶算法是对漏桶算法的一种改进,除了能够在限制调用的平均速率的同时还允许一定程度的流量突发。

总结

计数器固定窗口算法实现简单,容易理解。和漏斗算法相比,新来的请求也能够被马上处理到。但是流量曲线可能不够平滑,有“突刺现象”,在窗口切换时可能会产生两倍于阈值流量的请求。而计数器滑动窗口算法作为计数器固定窗口算法的一种改进,有效解决了窗口切换时可能会产生两倍于阈值流量请求的问题。

漏斗算法能够对流量起到整流的作用,让随机不稳定的流量以固定的速率流出,但是不能解决流量突发的问题。令牌桶算法作为漏斗算法的一种改进,除了能够起到平滑流量的作用,还允许一定程度的流量突发。

以上四种限流算法都有自身的特点,具体使用时还是要结合自身的场景进行选取,没有最好的算法,只有最合适的算法。

比如令牌桶算法一般用于保护自身的系统,对调用者进行限流,保护自身的系统不被突发的流量打垮。如果自身的系统实际的处理能力强于配置的流量限制时,可以允许一定程度的流量突发,使得实际的处理速率高于配置的速率,充分利用系统资源。而漏斗算法一般用于保护第三方的系统,比如自身的系统需要调用第三方的接口,为了保护第三方的系统不被自身的调用打垮,便可以通过漏斗算法进行限流,保证自身的流量平稳的打到第三方的接口上。

算法是死的,而算法中的思想精髓才是值得我们学习的。实际的场景中完全可以灵活运用,还是那句话,没有最好的算法,只有最合适的算法。

哈夫曼树

哈夫曼编码,又称为霍夫曼编码,它是现代压缩算法的基础,假设要把字符串【ABBBCCCCCCCCDDDDDDEE】转换成二进制编码进行传输,想到的第一种做法可以把他转换成 ASCII 编码(65~69,1000001~1000101),例如我们现在要传输 ABBB。

ABBB

1000001100001010000101000010

但是有点冗长,如果希望编码更短呢?

可以先约定一下五个字母对对应的二进制。

A  -> 0
B  -> 1
C  -> 10
D  -> 11
E  -> 100

我们依旧传输 ABBB

0111

但是现在有一个问题,我们如何去解释这个二进制呢?因为有的字符是 1 位有的是 2 位有的是三位的。这个二进制中指有 A 是 0 开头的,所以二进制的第一位是 0 我们可以直接解析成 A,但是后面的几位就很难搞了。

按照我们的规则 111 可以被解析为:

  • BD
  • DB
  • BBB

导致这个问题的根本原因是我们的一个字母的编码可以是后一个编码的前缀,所以我们就需要进行约定,每一个编码都不能是后一位的前缀。所以我们先规定,所有字母都用三位:

ABCDE
000001010011100

一共二十个字母,转换成 60 个二进制位.

如果使用哈夫曼编码,可以压缩至 41 个二进制位,约为原来长度的 68.3%。相当于压缩了30%。

哈夫曼树

先计算出每个字母的出现频率(权值,这里直接用出现次数),【ABBBCCCCCCCCDDDDDDEE】。

ABCDE
13862

利用这些权值,构建一颗哈夫曼树(又称为霍夫曼树、最优二叉树)。

如何构建一颗哈夫曼树呢?假设有 n 个权值。

  1. 以权值作为根节点构建 n 颗二叉树,组成森林
  2. 在森林中选出 2 个根节点最小的树合并,作为一颗新树的左右子树,且新树的根节点为其左右子树根节点之和。
  3. 从森林中删除刚刚选取的 2 颗树,并将新树加入森林
  4. 重复 2、3步骤,直到森林只剩一棵树为止,该树即为哈夫曼树。

根据规则,我们来构建哈夫曼树,构建过程如下图

2023-04-12T07:18:31.png

构建哈夫曼编码

以 left 为 0,right 为 1,可以得出 5 个字母对应的哈夫曼编 码

ABCDE
11101100101111

【ABBBCCCCCCCCDDDDDDEE】 的哈夫曼编码是:

11101101101100000000001010101010101111

现在还有一个问题,这个哈夫曼编码传递过去对方能解析吗,我们之前说要对齐编码长度,方便解析,可是在这里长度是不一样的。

哈夫曼的编码有一个特点,那就是的编码都不是其他编码的前缀,结合这一特点,我们就可以注意解析二进制了例如步骤如下:

  1. 我们先读去第一个 1 这样就可以将 C 排除了
  2. 继续读取,数据变成了 11,这样就把 D 给排除了
  3. 继续读取,数据变成了 111 这样就剩下了两个选择,A,E
  4. 继续读取,数据编程了 1110 这样就确定了是 A

以此类推就可以解码出所有的字符了。

在哈夫曼中出现次数最多的字符,编码是最短的,在上面的字符串中,C 出现的次数最多,所以它的编码就是 0,出现的最少的字符,编码就是最长的,这样一来,哈夫曼编码出现的二进制天然就是最短的,这样一来就构成了任何一个编码都不是另外编码的前缀。

总结

  • n 个权值构建出来的哈夫曼树拥有 n 个子叶子节点
  • 每个哈夫曼编码都不是另一个哈夫曼编码的前缀,说白了路径的长短,就是编码的长短,再结合上每个字符其实都是在叶子节点上,这就不可能是其他叶子节点的前缀,因为如果是前缀,需要满足两个节点的关系为父子节点才可以。
  • 哈夫曼是带全路径长度最短的树,权值较大的节点离根节点较近
  • 带权路径长度:书中所有的叶子结点的权值乘上其道根节点的路径长度。与最终的哈夫曼编码总长度成正比关系