maksim 发布的文章

在上一小节,我们使用 bat 脚本完成了服务的部署,那么在这一些小节我们将搭建 Nginx 来反向代理我们的 golang 应用,既然是私活级的项目,那肯定是越快上线越好,使用 Docker 我们可以快速上线各类应用。

使用 alpine 镜像启动 Golang 应用程序

上一节我们在构建Golang 的时候使用了 alpine3.16 那么我们在打包 Golang 镜像的时候最好与打包镜像保持一致,防止有冲突。

[root@localhost conf]# docker pull alpine:3.16
3.16: Pulling from library/alpine
213ec9aee27d: Already exists
Digest: sha256:bc41182d7ef5ffc53a40b044e725193bc10142a1243f395ee852a8d9730fc2ad
Status: Downloaded newer image for alpine:3.16
docker.io/library/alpine:3.16
[root@localhost conf]# docker images
REPOSITORY   TAG                 IMAGE ID       CREATED         SIZE
nginx        latest              2d389e545974   2 weeks ago     142MB
golang       1.18.6-alpine3.16   b68eed002951   3 weeks ago     328MB
alpine       3.16                9c6f07244728   7 weeks ago     5.54MB
nginx        1.19-alpine         a64a6e03b055   17 months ago   22.6MB

因为我们在上一节的构建镜像使用的就是 3.16 所以在 docker pull 的时候就直接用了本地的 layer,从此就可以看到 docker 的强大,并且 alpine 体积足够小只有5.54 MB。

接下来我们使用 docker 来运行 api-server。

docker run --name api-server -d -v /data/server/api-server/:/app -w /app alpine:3.16 ./ser
ver

一个在线生成 Nginx 配置的网站

it.baiked.com 提供了一个可以快速生成 nginx 配置的工具,点击链接进行调转

2022-10-03T07:47:40.png

首先,nginx 配置这个东西是很基础的,所有的开发者都应该去学习,但是这个东西如果在你已经会用了的情况下,还是能够给我们的日常工作一些帮助。

2022-10-03T07:48:04.png

我们先将隐藏 nginx 版本点选,然后修改配置文件路径 /etc/nginx/nginx.conf,点击增加server。

2022-10-03T07:48:14.png

在 server 中修改我们的域名,注意在规则中,我们将其设置为了 api/ 然后对程序部署目录和根目录进行修改,最后点击生成。

生成好的 config,如下:

# Generated by xubo it.baiked.com

user nginx;
pid /run/nginx.pid;
worker_processes auto;
worker_rlimit_nofile 65535;

events {
    multi_accept on;
    worker_connections 65535;
}

http {
    charset utf-8;
    tcp_nopush on;
    server_tokens off;
    types_hash_max_size 2048;
    client_max_body_size 1M;

    # MIME
    include mime.types;
    default_type application/octet-stream;

    # logging
    access_log /var/log/nginx/access.log;
    error_log /var/log/nginx/error.log warn;


    # load configs
    include /etc/nginx/conf.d/*.conf;

    # sihuo.maksim.website
    server {
        listen 80;
        listen [::]:80;
    
        server_name sihuo.maksim.website;
        set $base /usr/share/nginx;
        root $base/html;


        # api/
        location /api/ {
        
            proxy_pass http://127.0.0.1:8080/;
        
            proxy_set_header HOST $host;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
            proxy_set_header X-Real-IP $remote_addr;
        }
    }
}

安装 Nginx

我们需要将上面生成好的 nginx.conf 映射到 /etc/nginx/nginx.conf 中。

[root@localhost ~]# docker pull nginx:1.19-alpine

[root@localhost ~]# docker run --name nginx -d -v /data/data/nginx/conf/nginx.conf:/etc/nginx/nginx.conf -p 80:80 nginx:1.19-alpine

[root@localhost ~]# firewall-cmd --zone=public --add-port=80/tcp --permanent

[root@localhost ~]# firewall-cmd --reload

创建网络,让两个容器间进行通信

[root@localhost api-server]# docker network create -d bridge network-bridage
[root@localhost api-server]# docker network ls
NETWORK ID     NAME              DRIVER    SCOPE
e7f6b718f7dd   bridge            bridge    local
7f0c4539817b   host              host      local
1ea2a74e9f5c   network-bridage   bridge    local
1366058279d2   none              null      local

[root@localhost api-server]# docker network connect network-bridage api-server
[root@localhost api-server]# docker network connect network-bridge nginx-server

我们使用docker inspect 来查看容器的IP

[root@localhost api-server]# docker inspect api-server

"network-bridage": {
                    "IPAMConfig": {},
                    "Links": null,
                    "Aliases": [
                        "9b616e730cb0"
                    ],
                    "NetworkID": "1ea2a74e9f5ce9edaf32132a5b928c5859093baf4f5a76dbfcf172a1325ed395",
                    "EndpointID": "ed48e38a0e63b88fecc3030bcdc1604a10c460724ed4ee1ab5973a8ccc9bd769",
                    "Gateway": "172.18.0.1",
                    "IPAddress": "172.18.0.2",
                    "IPPrefixLen": 16,
                    "IPv6Gateway": "",
                    "GlobalIPv6Address": "",
                    "GlobalIPv6PrefixLen": 0,
                    "MacAddress": "02:42:ac:12:00:02",
                    "DriverOpts": {}
                }

然后修改nginx proxy_pass 的IP,最终在浏览器中查看

image-20221003070208270

小结

至此,我们的项目可以对外访问了,也完成了整个项目的 Docker 化,在这一小节中我们的部署方式也变成了 Docker ,但是脚本文件并没有进行修改,修改的部分我放到了下一章节,在下一章节,我们将解决线上的版本回滚。

我们在上一小节实现了应用的自动部署,但是其中有一个小问题,我们的部署依赖于线下打包,然后使用 scp 发给服务器,这个时候如果应用程序体积过大或者网络情况不好,那么就会非常耗时,你不能保证你时时刻刻都在一个非常良好的网络条件下。

这个时候我们就需要使用构建服务器了,但是使用 Jenkins 也需要新的服务器资源才可以,而且还需要进行学习,学习也是一个成本,最近呢,你还比较忙,真是抽不出来时间,那怎么办呢?

其实 Jenkins 的自动部署也是要依赖于我们的脚本的,我们使用 shell 完全可以解决这个问题,毕竟在没有自动化 CICD 之前,大家都是用 shell 解决的。

注意:这里是私活级!!!!

注意:这里是私活级!!!!

注意:这里是私活级!!!!

重要的事情说三遍,在一般的公司不会允许这么搞的!

想要在远程构建,首先我们需要解决代码如何同步到服务器上。

将代码上传至服务器

如果想要在线上进行打包,那么首先就需要将代码上传至服务器。

Goland 是一个强大的 IDE,其中为我们提供了上传代码的功能,这样一来我们就可以达到上传代码到服务器的目的。

在正常的公司里是肯定不允许这样操作的!

首先点击 Tools -> Deployment -> Configuration

2022-10-02T17:18:19.png

点击 + 号 -> sftp -> 输入 sever name

2022-10-02T17:19:17.png

开始填写 SFTP 内容,首先我们要新建 ssh 配置。

2022-10-02T17:20:44.png

因为我们之前已经添加了免密登录,所以我们在添加完IP信息后,选择 key pair ,他会默认帮我们选择好 id_rsa 文件,然后点击 Test Connection 看看能否链接服务器。

2022-10-02T17:20:53.png

回到 Deployment 配置页面我们点击 Root path配置目录,我们输入 /data/data。

2022-10-02T17:21:05.png

接下来点击 Mappings 来配置我们要将代码上传到哪个文件夹下,点击 Deployment path 并点击鼠标右键新建一个 src 的目录用于存放我们的源代码。

2022-10-02T17:21:20.png

然后再点击 Excluded Paths 来过滤掉不想上传的文件。

2022-10-02T17:21:31.png

build,.idea 两个目录其实是不用上传的,点击完成,接下来我们选中main.go 和 go.mod 这两个文件将其上传至 pro。

2022-10-02T17:21:40.png

接下来我们到 Linux 服务器上看一下代码是否已经被上传到服务器上。

[root@localhost src]# cd /data/data/src/ && ll
total 8
-rw-r--r--. 1 root root 1049 Oct  2 13:43 go.mod
-rw-r--r--. 1 root root  253 Oct  2 16:49 main.go

利用 Docker 构建应用程序

我们其实也可以在服务器上安装 golang 然后直接利用 go build 命令来构建应用程序,但是这其实涉及到污染生产环境的问题,所以我们采用 docker 的方式来进行打包,首先我们先安装 docker。

# 更新 yum 
sudo yum update

# 安装依赖
sudo yum install -y yum-utils device-mapper-persistent-data lvm2 -y

# 设置 docker 源
sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo

# 安装 docker
sudo yum install docker-ce -y

# 启动 docker
sudo systemctl start docker

# 随开机启动
sudo systemctl enable docker

# 查看 docker 版本
docker version

因为我们的本地 Golang 用的是1.18.6,所以我们在 docker hub 上选择了 1.18.6-apline3.16 作为我们的打包工具。

选择 alpine 版本还有一个原因就是它的体积是非常小的,既然是“私活”,那就应该从各个方面来思考如何节省资源。

[root@localhost src]# docker pull golang:1.18.6-alpine3.16

如果你觉着 pull 的速度太慢,那可以换成国内的源,这里我就不赘述了。

下载完成后,我们来执行 docker run 命令来打包试试:

[root@localhost src]# docker run --rm -it \
-v /data/data:/data/data \ 
-w /data/data/src/ \
 e CGO_ENABLED=0 \
-e GOPROXY=https://goproxy.cn \
golang:1.18.6-alpine3.16 go build -o ../server-build-docker main.go

go: downloading github.com/gin-gonic/gin v1.8.1
go: downloading github.com/gin-contrib/sse v0.1.0
go: downloading github.com/mattn/go-isatty v0.0.14
go: downloading golang.org/x/net v0.0.0-20210226172049-e18ecbb05110
go: downloading github.com/go-playground/validator/v10 v10.10.0
go: downloading github.com/pelletier/go-toml/v2 v2.0.1
go: downloading github.com/ugorji/go/codec v1.2.7
go: downloading google.golang.org/protobuf v1.28.0
go: downloading gopkg.in/yaml.v2 v2.4.0
go: downloading golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069
go: downloading github.com/go-playground/universal-translator v0.18.0
go: downloading github.com/leodido/go-urn v1.2.1
go: downloading golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97
go: downloading golang.org/x/text v0.3.6
go: downloading github.com/go-playground/locales v0.14.0

--rm 在docker 容器执行完成后会直接被删掉,我们首先是将/data/data 目录挂在到容器中,之后将工作目录切换到 /data/data/src 目录下进行打包操作。

[root@localhost data]# cd /data/data && ls -l
total 9880
-rwxr-xr-x. 1 root   root   10110954 Oct  2 23:42 server-build-docker
-rw-rw-r--. 1 maksim maksim     2783 Oct  2 17:20 server.log
drwxr-xr-x. 2 root   root         49 Oct  2 23:41 src

我们可以看到文件被生成了,并且被设置成了可执行文件。

这里存在一个问题,每一次都会去重新下载依赖,这样会影响打包的速度,我们可以将 gopath 目录找到然后将其目录挂载出来。

# 创建目录用来保存 go build 的依赖文件
mkdir /data/data/godep

# 执行新的 go build 命令
docker run --rm -it -v /data/data:/data/data \ 
-v /data/data/godep:/go \
-w /data/data/src/ \
 e CGO_ENABLED=0 \
-e GOPROXY=https://goproxy.cn \
golang:1.18.6-alpine3.16 go build -o ../server-build-docker main.go

# 第二次执行
docker run --rm -it -v /data/data:/data/data \ 
-v /data/data/godep:/go \
-w /data/data/src/ \
 e CGO_ENABLED=0 \
-e GOPROXY=https://goproxy.cn \
golang:1.18.6-alpine3.16 go build -o ../server-build-docker main.go

在我们第二次执行的时候就不会再去下载依赖了。

改进编译、部署脚本

这个时候我们再去完善我们的部署脚本,我们新建一个 remote-build-deploy 脚本,完成构建以及部署操作。

:: 构建应用
ssh [email protected] "bash /data/data/src/remote-build.sh"

这一次,我们的脚本变成了一个行,所有的编译环节的脚本我们都放到了服务器上,这是因为 ssh 单条执行命令的效率实在是太低了,并且部署脚本单独抽出来,我们可以在其中做一些逻辑判断,如下:

#!/bin/bash

build_path=/data/data/server-build-docker

build() {
  echo "开始执行编译"

  docker run --rm -i \
    -v /data/data:/data/data \
    -v /data/data/godep:/go \
    -w /data/data/src/ \
    -e GOPROXY=https://goproxy.cn \
    -e CGO_ENABLED=0 \
    golang:1.18.6-alpine3.16 go build -o $build_path main.go

  if [ -f $build_path ]; then
    echo "build 成功:目录 "$build_path
  else
    echo "build 失败,没有找到该文件"
        exit 20
  fi
}

build

当完成后,我们去判断是否有文件产出,也就是我们的常说的"制品",如果没有就赶紧报错!

之后就是部署脚本,与构建脚本一样,部署脚本我们也放到了远程执行。

:: 部署应用
ssh [email protected] "bash /data/data/src/remote-deploy.sh"
#!/bin/bash

server_path=/data/server/api-server/server
build_path=/data/data/server-build-docker

deploy() {
  if [ ! -f $build_path ]; then
    echo "build 失败,没有找到该文件"
    exit 20
  fi

  process=$(ps -ef | grep $server_path | grep -v "grep" | wc -l)

  if [[ process == 0 ]]; then
    echo "当前没有执行,可以直接启动,启动中...."
    # 进程不存在直接移动目录4
    mv $build_path $server_path
    nohup $server_path &>>/data/data/server.log 2>&1 &
  else
    echo "当前有执行,杀死进程"
    ps -ef | grep server | awk '{print $2}' | xargs kill -9
    echo "杀死成功,执行运动命令"
    mv $build_path $server_path
    echo "开始执行"
    nohup $server_path &>>/data/data/server.log 2>&1 &
  fi
  echo "执行成功";

}

test() {
    sleep 2
    curl localhost:8080
    echo ""
}

deploy

test
  1. 首先校验的是是否有制品存在
  2. 如果有制品,判断当前是否有正在执行的进程
  3. 如果有在执行的进程,那么就干掉,然后移动、执行。
  4. 如果没有则直接移动、执行

小结

到目前为止,我们完成了自动化远程 docker 编译,在下一小节,我们来实现 api-server 的 docker 化。

0.09x 是目前能够下载到的最古老的版本,在代码中有标注,这是一个预览版,所有代码加起来在 1W 多行,其中包括了文档,和一些辅助脚本,第三方案例,真正的 C 语言实现的代码仅有 5200 多行,这其中其实已经包含了很多 Redis 的核心逻辑,阅读起来的障碍也会小很多。

Reids 7 的C 代码行数已经超过了15W行,阅读起来的难度可想而知。

2023-03-14T09:01:22.png

我们先来看一下 redis 的目录结构

2023-03-14T09:01:37.png

这个时候的目录非常简洁,工程目录下直接存放了所有的源代码,以及几个目录。

  • client-libraries 存放了各个语言的 client sdk,我们直接可以利用其中的 sdk 进行与 redis-server 的交互。
  • doc 存放了 redis 的所有文档。
  • utils 是 redis 的工具包,使用 ruby 进行编写的。

这个时候的 Makefile 异常简洁,没有后面那么复杂。一共只有 62 行,只要拥有一定的 C 语言基础相信直接就能看明白。

# Redis Makefile
# Copyright (C) 2009 Salvatore Sanfilippo <antirez at gmail dot com>
# This file is released under the BSD license, see the COPYING file

DEBUG?= -g
CFLAGS?= -std=c99 -pedantic -O2 -Wall -W -DSDS_ABORT_ON_OOM
CCOPT= $(CFLAGS)

OBJ = adlist.o ae.o anet.o dict.o redis.o sds.o zmalloc.o lzf_c.o lzf_d.o
BENCHOBJ = ae.o anet.o benchmark.o sds.o adlist.o zmalloc.o
CLIOBJ = anet.o sds.o adlist.o redis-cli.o zmalloc.o

PRGNAME = redis-server
BENCHPRGNAME = redis-benchmark
CLIPRGNAME = redis-cli

all: redis-server redis-benchmark redis-cli

# Deps (use make dep to generate this)
adlist.o: adlist.c adlist.h
ae.o: ae.c ae.h
anet.o: anet.c anet.h
benchmark.o: benchmark.c ae.h anet.h sds.h adlist.h
dict.o: dict.c dict.h
redis-cli.o: redis-cli.c anet.h sds.h adlist.h
redis.o: redis.c ae.h sds.h anet.h dict.h adlist.h zmalloc.c zmalloc.h
sds.o: sds.c sds.h
sha1.o: sha1.c sha1.h
zmalloc.o: zmalloc.c

redis-server: $(OBJ)
    $(CC) -o $(PRGNAME) $(CCOPT) $(DEBUG) $(OBJ)
    @echo ""
    @echo "Hint: To run the test-redis.tcl script is a good idea."
    @echo "Launch the redis server with ./redis-server, then in another"
    @echo "terminal window enter this directory and run 'make test'."
    @echo ""

redis-benchmark: $(BENCHOBJ)
    $(CC) -o $(BENCHPRGNAME) $(CCOPT) $(DEBUG) $(BENCHOBJ)

redis-cli: $(CLIOBJ)
    $(CC) -o $(CLIPRGNAME) $(CCOPT) $(DEBUG) $(CLIOBJ)

.c.o:
    $(CC) -c $(CCOPT) $(DEBUG) $(COMPILE_TIME) $<

clean:
    rm -rf $(PRGNAME) $(BENCHPRGNAME) $(CLIPRGNAME) *.o

dep:
    $(CC) -MM *.c

test:
    tclsh test-redis.tcl

bench:
    ./redis-benchmark

log:
    git log '--pretty=format:%ad %s' --date=short > Changelog

Redis 的入口

我们直接到 redis 的入口函数,其代码在 redis.c 中,直接拖拽滚动框到最底部即可,我对代码的每个阶段做了注释。启动一个 redis 服务其实 3 个阶段总计 8 个步骤。

  • 阶段一:基本初始化

    1. 初始化 server 配置,initServerConfig()。
    2. 判断是否有指定配置文件,如果有则重新加载配置文件,loadServerConfig()。
    3. 初始化服务,initServer()。
    4. 判断是否为后台执行,daemonize()。
  • 阶段二:检查 RDB

    1. 加载 rdb 文件,当进程退出后会加载以前保存的 rdb 文件,rdbLoad()。
  • 阶段三:事件驱动

    1. 创建 event-driven 其实就是解析客户端请,并且将 socket fd 放入到事件驱动当中去,aeCreateFileEvent()。
    2. 执行死循环,接收请求,其实这一步就是我们说 redis 是单线程的原因,aeMain()。
    3. acMain 判断是否停止,如果停止则删除事件,并退出程序,aeDeleteEventLoop()。
int main(int argc, char **argv) {
    //1. 初始化 server 段配置
    initServerConfig();
    //2. 判断是否有指定配置文件,如果有则重新加载配置文件
    if (argc == 2) {
        ResetServerSaveParams();
        loadServerConfig(argv[1]);
    } else if (argc > 2) {
        //如果参数过多则报错,显示正确的配置
        fprintf(stderr, "Usage: ./redis-server [/path/to/redis.conf]\n");
        exit(1);
    }
    //3. 初始化服务
    initServer();
    //4. 判断是否为后台执行
    if (server.daemonize) daemonize();
    redisLog(REDIS_NOTICE, "Server started, Redis version " REDIS_VERSION);
    //5. 加载 rdb 文件,当进程退出后会加载以前保存的 rdb 文件
    if (rdbLoad(server.dbfilename) == REDIS_OK)
        redisLog(REDIS_NOTICE, "DB loaded from disk");
    //6. event-driven 其实就是解析客户端请求
    if (aeCreateFileEvent(server.el, server.fd, AE_READABLE,
                          acceptHandler, NULL, NULL) == AE_ERR)
        oom("creating file event");
    redisLog(REDIS_NOTICE, "The server is now ready to accept connections on port %d", server.port);
    //7. 死循环执行
    aeMain(server.el);
    //8. 当接收到退出信号后,删除事件
    aeDeleteEventLoop(server.el);

    return 0;
}

阶段一:基本初始化

initServerConfig 初始化 Server 配置

initServerConfig 就是对 redis 进行默认值的初始化。

static void initServerConfig() {
    # 初始化 redis 默认 DB 数量,默认是 16 个
    server.dbnum = REDIS_DEFAULT_DBNUM;
        # 初始化 redis 默认端口 3306
    server.port = REDIS_SERVERPORT;
    # 日志级别,默认是 debug
    server.verbosity = REDIS_DEBUG;
    # 默认超时时间
    server.maxidletime = REDIS_MAXIDLETIME;
    # 存储策略
    server.saveparams = NULL;
    server.logfile = NULL; /* NULL = log on standard output */
    server.bindaddr = NULL;
        # 设置在向客户端应答时,是否把较小的包合并为一个包发送,默认为开启
    server.glueoutputbuf = 1;
    server.daemonize = 0;
    server.pidfile = "/var/run/redis.pid";
    server.dbfilename = "dump.rdb";
    # 是否要求通行证
    server.requirepass = NULL;
        # 对象共享池
    server.shareobjects = 0;
    # 重置保存策略
    ResetServerSaveParams();
    appendServerSaveParams(60 * 60, 1);  /* save after 1 hour and 1 change */
    appendServerSaveParams(300, 100);  /* save after 5 minutes and 100 changes */
    appendServerSaveParams(60, 10000); /* save after 1 minute and 10000 changes */
    /* 主从复制相关的配置 */
    server.isslave = 0;
    server.masterhost = NULL;
    server.masterport = 6379;
    server.master = NULL;
    server.replstate = REDIS_REPL_NONE;
}

ResetServerSaveParams() 的操作这里很简单,其实就是将原有的内存空间清空。

static void ResetServerSaveParams() {
    zfree(server.saveparams);
    server.saveparams = NULL;
    server.saveparamslen = 0;
}

在这里,在 redis 0.091 版本中,redis 对内存分配使用的还是 zmalloc,这个我们放在后面去讲解,

server 是一个全局静态变量,数据类型是 redisServer。

loadServerConfig 重新加载配置文件

重新加载配置文件就很好理解了。

   if (argc == 2) {
        ResetServerSaveParams();
        loadServerConfig(argv[1]);
    } else if (argc > 2) {
        //如果参数过多则报错,显示正确的配置
        fprintf(stderr, "Usage: ./redis-server [/path/to/redis.conf]\n");
        exit(1);
    }

只是一段非常普通的 if 判断。

如果参数是两个,那么就说明有配置文件,执行加载逻辑

  1. 重新初始化 Server Save 参数
  2. loadServerConfig

loadServerConfig 由于代码太长了,而且大多数都是参数解析,在这里就不全都贴出来了,我们来看一些核心的逻辑。

//跳过了获取文件,判断文件是否有效的逻辑
while (fgets(buf, REDIS_CONFIGLINE_MAX + 1, fp) != NULL) {
        sds *argv;
        int argc, j;

        linenum++;
        line = sdsnew(buf);
        line = sdstrim(line, " \t\r\n");
/* Skip comments and blank lines*/
        if (line[0] == '#' || line[0] == '\0') {
            sdsfree(line);
            continue;
        }

        /* Split into arguments */
        argv = sdssplitlen(line, sdslen(line), " ", 1, &argc);
        sdstolower(argv[0]);

        /* Execute config directives */
        if (!strcmp(argv[0], "timeout") && argc == 2) {
            server.maxidletime = atoi(argv[1]);
            if (server.maxidletime < 1) {
                err = "Invalid timeout value";
                goto loaderr;
            }
                 } else if (xxxx) {
                        .....
                 } else .....
return;
    loaderr:
    fprintf(stderr, "\n*** FATAL CONFIG FILE ERROR ***\n");
    fprintf(stderr, "Reading the configuration file, at line %d\n", linenum);
    fprintf(stderr, ">>> '%s'\n", line);
    fprintf(stderr, "%s\n", err);
    exit(1);
}

在这段,出现了我们 redis 的第一个数据结构——sds,也就是我们在缓存中使用最多的字符串类型。

在 Redis0.091 中,SDS 的实现还是非常简单的,具体的 SDS 操作,我们在后续的部分进行讲解,我们先了解一下这个数据结构。头文件加实现代码一共才 275 行。

2023-03-14T09:02:12.png

Reids 利用 SDS 代替的字符串,这涉及到字符串的二进制安全问题。,C语言中,用“\0”表示字
符串的结束,如果字符串中本身就有“\0”字符,字符串就会被截断,即非二进制安全;若通过某种机制,保证读写字符串时不损害其内容,则是二进制安全。

在 C 语言中,字符串以 \0 结尾,这也就意味着字符串中如果出现了\0就会破坏数据的完整性,字符串也就会被截断。

为了解决这样的问题,redis 将 sds 设计成下面的样子。

typedef char *sds;

struct sdshdr {
    long len;
    long free;
    char buf[];
};
  • len 属性为5,表示这个SDS保存着一个5字节长的字符串,由于长度统计变量的存在,读取字符串不依赖”\0”终止符,保证了二进制安全。
  • free 属性为0,表示这个SDS没有分配任何未使用空间。
  • buf:内容存储在柔性数组 buf中,SDS 对上层暴露的指针不是指向结构体 SDS 的指针,而是直接只想荣幸数组的指针,上层就可以像读取 C 字符串一样读取 SDS 的内容,兼容 C 语言处理字符串的各种函数。

2023-03-14T09:02:32.png

柔性数组只能放在结构体的尾部。柔性数组的地址和接头体是连续的,这样查找内存更快,因为不需要额外通过指针找到字符串为止,可以方便地通过柔性数组的首地址偏移得到结构体首地址,进而方便获取其余变量。

总结一下 SDS 和 Char 的区别

  1. 通过使用SDS字符串(len记录字符串长度),使得获取字符串长度的复杂度从O(N)变为O(1)
  2. 杜绝缓存区溢出,C字符串不记录自身长度,在拼接字符串时可能造成缓存区溢出
  3. 通过未使用空间free,减少修改字符串带来的内存重分配次数

其实这种设计在其他项目中也存在,这是 PHP3 中对 string 类型的视线。

2023-03-14T09:02:47.png

当校验没有问题后,就开始抓好难过解析配置,如果配置正确就继续扫描下一行,如果错误就会利用 goto 跳转到 loaderr。

可能在很多 C 语言的帖子中或者老师的课程中都不允许使用 goto 关键字,但是在实际的项目中这种用法有很多,并不是绝对不可以使用,因为在 C语言中并没有异常机制,像这种利用 goto 特性跳转到输出位置进行输出信息,处理异常会特别方便,代码逻辑也更加的清晰,在处理异常的同事也可以进行回收资源。

老师说的意思其实是不要多层跳转,因为多层跳转会让代码变得混乱。

initServer() 初始化 server

得到配置后,万事俱备只欠东风,我们开始对 server 进行初始化。

static void initServer() {
    int j;

    // 屏蔽信号,避免异常
    signal(SIGHUP, SIG_IGN);
    signal(SIGPIPE, SIG_IGN);

   
    server.clients = listCreate();
    server.slaves = listCreate();
    server.monitors = listCreate();
    server.objfreelist = listCreate();

    // 创建 SharedObjects 这里就是存储 redis 数据的地方;
        //  如果有熟悉Redis的朋友可能会知道 RedisObject,这个就是它的前身。
    createSharedObjects();

        //创建事件驱动
    server.el = aeCreateEventLoop();
    server.db = zmalloc(sizeof(redisDb) * server.dbnum);
    server.sharingpool = dictCreate(&setDictType, NULL);
    server.sharingpoolsize = 1024;

        // 如果分配内存失败也就意味着没有可用空间了,那就 OOM
    if (!server.db || !server.clients || !server.slaves || !server.monitors || !server.el || !server.objfreelist)
        oom("server initialization"); /* Fatal OOM */

    #创建 TCP 连接
    server.fd = anetTcpServer(server.neterr, server.port, server.bindaddr);
    if (server.fd == -1) {
        redisLog(REDIS_WARNING, "Opening TCP port: %s", server.neterr);
        exit(1);
    }

    for (j = 0; j < server.dbnum; j++) {
        server.db[j].dict = dictCreate(&hashDictType, NULL);
        server.db[j].expires = dictCreate(&setDictType, NULL);
        server.db[j].id = j;
    }

    server.cronloops = 0;
    server.bgsaveinprogress = 0;
    server.lastsave = time(NULL);
    server.dirty = 0;
    server.usedmemory = 0;
    server.stat_numcommands = 0;
    server.stat_numconnections = 0;
    server.stat_starttime = time(NULL);
    aeCreateTimeEvent(server.el, 1000, serverCron, NULL, NULL);
}
  • 屏蔽信号
  • 初始化 clients,slaves,monitors,objfreelist 四个链表。

    • clients 客户端
    • slaves 从库
    • monitors 监控器,某个 client 执行 monitors 的时候会变成一个监控器,服务端执行的所有命令都会发送给监控器。
    • objfreelist 空闲对象池
  • 初始化 dict 也就是 shareobjects 存储键值对的地方
  • 创建 tcp 连接
  • 创建数据库,其中会初始化 dict,过期时间 dict
  • 初始化 server 剩余的属性
  • 注册听时期

listCreate() 引出了,我们的第二个数据结构——双向链表。

2023-03-14T09:14:02.png

其代码量加载一起一共是 214 行,核心主要是下面两个结构体,list、ListNode 构成了 redis 中的双向链表。

typedef struct list {
    listNode *head;
    listNode *tail;
    void *(*dup)(void *ptr);
    void (*free)(void *ptr);
    int (*match)(void *ptr, void *key);
    unsigned int len;
} list;

typedef struct listNode {
    struct listNode *prev;
    struct listNode *next;
    void *value;
} listNode;

在链表中保存着 head 和 tail,也就是链表中的头和尾。每个 Node 节点都保存着前一个和后一个的节点指针。

通过单个节点我们不仅能够访问到下一个节点,还能访问到上一个节点。使用双链表而非单链表的目的是想增加结构的灵活性,因为只有经常对数据进行增加或者删除才会用到链表这种数据结构。无论是增加节点抑或删除节点,双链表都要比单链表更加简单高效。但提高灵活性的代价是需要更多的空间,但这个代价小到可以被我们忽略。

daemonize 守护进程

static void daemonize(void) {
    int fd;
    FILE *fp;

    if (fork() != 0) exit(0); /* parent exits */
    setsid(); /* create a new session */

    /* Every output goes to /dev/null. If Redis is daemonized but
     * the 'logfile' is set to 'stdout' in the configuration file
     * it will not log at all. */
    if ((fd = open("/dev/null", O_RDWR, 0)) != -1) {
        dup2(fd, STDIN_FILENO);
        dup2(fd, STDOUT_FILENO);
        dup2(fd, STDERR_FILENO);
        if (fd > STDERR_FILENO) close(fd);
    }
    /* Try to write the pid file */
    fp = fopen(server.pidfile, "w");
    if (fp) {
        fprintf(fp, "%d\n", getpid());
        fclose(fp);
    }

这是一个标准的守护进程代码。

  1. fork 子进程,父进程退出
  2. 设置会话 id
  3. 转移标准输入,标准输出,标准错误
  4. 将 pid 放到文件中

阶段二:rdbLoad 加载 rdb 文件

函数loadDb 的实现完全是根据写入磁盘数据库文件的rdbSave函数来实现的, 后续我们在定时事件中再看rdbSave实现,在此略过分析loadDb的实现。

阶段三:事件驱动

事件驱动框架是 Redis Server 运行的核心,该框架爱一单启动后就会一直执循环执行,每次循环都会处理一批出发的网络读写事件。

其实进入事件框架开始执行并不复杂,main 函数直接调用时间框架爱的主题函数 aeMain 后就进入了时间处理循环了。

//7. 死循环执行
aeMain(server.el);

而他的源代码实现更简单。

void aeMain(aeEventLoop *eventLoop)
{
    eventLoop->stop = 0;
    while (!eventLoop->stop)
        aeProcessEvents(eventLoop, AE_ALL_EVENTS);
}

其实就是一个死循环,如果当前的事件驱动的 stop 属性等于 1 了就说明服务终止,那么就退出循环,整个业务逻辑就结束了。

//8. 当接收到退出信号后,删除事件
aeDeleteEventLoop(server.el);

在 0.091 这个版本中还没有引入 epoll 的支持,底层使用的还是 select。

2023-03-14T09:11:15.png

所以在这个版本中,如果没有做内核优化,那么 redis 的并发 fd 最多也就是 1024 个,远没有后面那么高的并发量。

到目前为止,我们算是知道了 redis0.091 的入口,接下来,我们会进入到具体的实现细节当中,下一章,我们来看 redis server 是如何处理用户请求的。

前言

大家好,我是一笑,在这篇文章中将为大家介绍如何利用 Docker 部署私活级 Golang 程序,最终我们要通过最低的成本来实现 goalng 的自动打包,部署操作。

首先我们来定义一下什么是私活级,私活大家都懂,就是我们利用业余去赚一些外快,一般情况下后端基本上都是一个人独立完成的,有的时候甚至真的只有一个人,没有运维,没有测试,一个人要负责整个软件的生命周期,从开发迭代到后面的运维部署,所以在本文中私活级将指代是没有企业级运维能力的研发场景

在这种情况,我们可以利用 Docker 和 脚本命令来完成一些简单的自动化操作,来减少我们的时间成本以及运维成本。

由于刚开始是一个人进行研发工作,所以我们并不会涉及到 gitlab, Jenkins 这些 DevOPS 工具,毕竟这些会增加我们的研发成本,如果非要部署这些服务,我的建议是可以使用树莓派,同时将 gitlab 更换为 gitea ,因为 gitea 可以更加节省服务器资源,同时一定要定时备份,这点很重要,以免树莓派损坏导致的数据丢失。

在后面我会不断追加这一系列的文章,其中也会讲到 DevOPS , 不过这些不是这篇文章的主要内容,在这里不做过多说明,接下来让我们正式开始私活级 Golang 程序的部署工作。

环境准备

这里为了节省成本,就不从云服务厂商购买服务器了,使用虚拟机来进行搭建。

  • Golang 1.18
  • Goland IDE
  • VitralBox
  • Windows(我本机使用的是 Windows 11)
虚拟机操作系统网络模式虚拟机 IP虚拟机 配置
CentOS 7桥接网卡192.168.1.100cup: 2 mem:2G

基础 Go 服务发布

代码准备

首先,我们来初始化我们的项目代码,因为这里的项目并不重要,所以我们用 gin 框架简单输出一个 Hello World 来模拟我们的项目代码,项目结构如下:

PS C:\Users\maksim\WorkSpace> tree .\proejct\
文件夹 PATH 列表
卷序列号为 5402-60A2
│  go.sum
│  main.go

main.go:

package main

import (
    "github.com/gin-gonic/gin"
    "log"
)

func main() {
    r := gin.Default()
    r.Handle("GET", "/", func(context *gin.Context) {
        context.String(200, "hello world!")
    })
    err := r.Run(":8080")

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

代码很简单,就是利用 gin 返回 hello world!,我们将以此为基础来进行部署,首先我们不使用 docker 进行部署,而是通过 ssh 将 编译好的代码同步到服务器上。

由于我的开发机是 Windows, 所以这个时候需要进行交叉编译,在Windows 平台上编译 Linux 可执行文件,我们先来编写一个 build.bat 脚本,用来辅助我们自动化生成可执行文件,其脚本路径与 mian.go 同级。

set GOOS=linux
set GOARCH=amd64
go build -o build/server main.go

我们来执行一下脚本文件。

# 执行 build 脚本生成可执行文件
PS C:\Users\maksim\WorkSpace\proejct> .\build.bat

C:\Users\maksim\WorkSpace\proejct>set GOOS=linux

C:\Users\maksim\WorkSpace\proejct>set GOARCH=amd64

C:\Users\maksim\WorkSpace\proejct>go build -o build/server main.go

# 查看 build 目录下文件

PS C:\maksim\guozh\WorkSpace\proejct> ls .\build\


    目录: C:\Users\maksim\WorkSpace\proejct\build


Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
-a----        2022-10-02     14:00       10062347 server

部署 Linux

我的 Linux 安装选用了最小化安装,简单配置了IP,没有对 Linux 进行用户设置,所以用 root 用户进行登录,所以为了安全第一件事就是为 Linux 设置新的账户。

# 1. ssh 登录
ssh [email protected]

# 2. 输入密码,这里略过

# 3. 新建账户
# 3.1 新增用户
useradd -m maksim
# 3.2 给用设置密码
passwd maksim

# 3.3 给 maksim  root 权限
cat >> /etc/sudoers <<EOF              #添加用户可以知晓所有命令(可以根据需要修改),切换不用输入密码
user1   ALL=(ALL)      NOPASSWD: ALL
EOF

# 4. 给新用户 SSH 登录权限
# 4.1 编辑 sshd_config 文件
vi /etc/ssh/sshd_config
# 4.2 找到 AllowUsers, 解除注释,并且加入用户名,用户名之间用空格隔开
AllowUsers username username2
# 4.3 重启 ssdh 服务
service sshd restart

这样主要是为了 Linux 的系统安全,在线上拥有 root 权限其实是一个非常可怕的情况,毕竟如果你想的话 rm -rf /* 也是完全没有问题的,所以一般公司都不会给开发人员 root 权限,线上一般也就是给只读权限的账户。

现在我们已经拥有了新的账户,接下来我们来部署应用,现在使用新的账户进行操作。

# 在根目录下新建data 目录,其中包括 soft,data,server
sudo mkdir -p /data/{soft,data,server}

[maksim@localhost data]$ cd /data/ && ls -l
total 0
drwxr-xr-x. 2 root root 6 Oct  2 14:29 data
drwxr-xr-x. 2 root root 6 Oct  2 14:29 server
drwxr-xr-x. 2 root root 6 Oct  2 14:29 soft
  • soft 目录是用来存放我们安装的应用,例如 redis, MySQL, nginx
  • data 用来存放应用数据,例如Nginx 日志,应用日志等。
  • server 用来存放我们的应用服务,也就是我们要部署的内容;

我们在 soft 目录下新建 api-server 目录,该目录用来存放我们的 helloworld 程序。

cd server  && sudo mkdir api-server

创建完目录后,我们来设置免密登录,这是因为我们需要上传我们的应用程序,如果是使用 scp 上传每一次都需要传入密码,就很麻烦。

首先我们打开 cmd 生成本机的 ssh 密匙。

PS C:\Users\maksim>  ssh-keygen -t rsa -C "[email protected]"
Generating public/private rsa key pair.
Enter file in which to save the key (C:\Users\maksim/.ssh/id_rsa):
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in C:\Users\maksim/.ssh/id_rsa
Your public key has been saved in C:\Users\maksim/.ssh/id_rsa.pub
The key fingerprint is:
SHA256:pwc27Wi3bpeGM8vZJ6MoM2Hoz5MWMU1SZdMl3cfVYLY [email protected]
The key's randomart image is:
+---[RSA 3072]----+
|       ...+..o=++|
|      . .. ..+..=|
|       +      E .|
|      o ..       |
|     . oS o      |
|    . +. B       |
|   . . ++ +. .   |
|    ..B. ==+* .  |
|     oo=.oBB.+   |
+----[SHA256]-----+

ssh-keygen -t rsa -C "[email protected]" 这个命令的具体意义,大家可自行百度,这里不做赘述,因为这篇文章的重点不在这里。

 PS C:\Users\maksim\.ssh> type .\id_rsa.pub

ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC8gUVLCs1Vbei3L6lYWrr7QTWee/qEnC9t2oNeiSKsQMtRTRUh+oCIqSR6zL+5giK1bthRUJnKaJZqrJPSsdaiispaoduoiIsklfdjJLIOJI@saslE= [email protected]

我们可以到查看该目录下的 ssh ,在这里的ssh 请不要复制,因为是我瞎写的。

我们将生成好的 id_rsa.pub 文件内容添加到 Linux 中。

PS C:\Users\maksim\.ssh> type ./id_rsa.pub | ssh [email protected] "mkdir .ssh && cat >> .ssh/authorized_keys"
注意:这里的脚本中有一段是 mkdir .ssh 这是因为我的虚拟机中没有该目录,如果报错了的话,可以将 makdir .ssh && 删掉。

在这里我们使用了 root 账户,这是因为可以避免很多权限问题。

我们来测试一下是否可以免密码登录:

PS C:\Users\maksim\.ssh> ssh [email protected]
Last login: Sun Oct  2 16:18:41 2022
[root@localhost ~]#

现在我们来编写 deploy.sh 脚本,用来部署我们的应用程序:

scp ./build/server root:/data/server/api-server/server

我们通过 scp 将编译好的目录上传上去,我们执行完成后就会发现,server 文件就被上传到了我们的Linux当中:

[root@localhost api-server]# ls -l
total 9828
-rw-r--r--. 1 root root 10062347 Oct  2 16:39 server

但是这个时候,细心的你发现这个文件没有可执行权限啊,很简单,我们来继续晚上 deploy.bat 脚本,我们使用 ssh 命令 来给该文件配置权限。

set server_path=/data/server/api-server/server

scp ./build/server [email protected]:%server_path%

set chmod_exec_cmd= chmod +x %server_path%

ssh [email protected] %chmod_exec_cmd%

这是我们修改后的脚本,使用 bat 变量来配置要上传的路径,并且在最后执行了权限修改命令,我们来执行一下,再看看 Linux 中 server 的变化。

[root@localhost api-server]# ls -l
total 9828
-rwxr-xr-x. 1 root root 10062347 Oct  2 16:45 server

我们可以看到,这样一来 server 就具备了可执行权限,我们可以执行一下 server。

[maksim@localhost api-server]$ ./server
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.

[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:   export GIN_MODE=release
 - using code:  gin.SetMode(gin.ReleaseMode)

[GIN-debug] GET    /                         --> main.main.func1 (3 handlers)
[GIN-debug] [WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.
Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.
[GIN-debug] Listening and serving HTTP on :8080

我们先让它在后台运行

#我们现在用的是 maksim 账户,但是创建目录的时候用的 root ,所以我们需要将权限转交给 maksim 避免出现权限错误
[maksim@localhost api-server] chown -R /data/data maksim

[maksim@localhost api-server] nohup ./server &>> /data/data/server.log 2>&1 &

运行后我们执行 curl 命令来判断程序是否运行成功:

[maksim@localhost api-server]$ curl localhost:8080
hello world!

在这里还有一点需要进行优化,那就是我们的服务如果在运行阶段是无法重新部署的,我们可以试验一下:

[maksim@localhost api-server]$ ls -l
total 9828
-rwxr-xr-x. 1 root root 10062347 Oct  2 17:10 server

现在最新的上传时间是17:10,我们来执行上传脚本,然后在观察时间的变化。

PS C:\Users\maksim\WorkSpace\proejct> .\delopy.bat

C:\Users\maksim\WorkSpace\proejct>set server_path=/data/server/api-server/server

C:\Users\maksim\WorkSpace\proejct>scp ./build/server [email protected]:/data/server/api-server/server
scp: /data/server/api-server/server: Text file busy

C:\Users\maksim\WorkSpace\proejct>set chmod_exec_cmd= chmod +x /data/server/api-server/server

C:\Users\maksim\WorkSpace\proejct>ssh [email protected]  chmod +x /data/server/api-server/server

我们可以看到,这个时候的脚本执行是没有报错的,但是我们在 Linux 中执行 ls -l

[maksim@localhost api-server]$ ls -l
total 9828
-rwxr-xr-x. 1 root root 10062347 Oct  2 17:10 server

所以我们需要将 Linux 中的进程杀死,然后在重新启动脚本。

:: 设置服务文件路径
set server_path=/data/server/api-server/server

:: 将进程杀死
ssh [email protected] "ps -ef | grep server | awk '{print $2}' | xargs kill -9"

:: 发送应用程序文件
scp ./build/server [email protected]:%server_path%

:: 设置执行权限
set chmod_exec_cmd= chmod +x %server_path%
ssh [email protected] %chmod_exec_cmd%
ssh [email protected] "nohup %server_path% &>> /data/data/server.log 2>&1 &"

我们执行完成后再去 Linux 上看一下。

[maksim@localhost api-server]$ ps -ef | grep server
root      2210     1  0 17:19 ?        00:00:00 /data/server/api-server/server
maksim    2216  1867  0 17:19 pts/1    00:00:00 grep --color=auto server
[maksim@localhost api-server]$ curl localhost:8080
hello world!

好了,到目前为止,我们的开发,打包,部署就已经基本完成了,下面我们开始对项目进行 docker 化,如果你不想打包 docker 那么到这里你还有以下几点需要去做:

  1. 使用 supervisor 监控应用程序
  2. 如果真的只有一台节点,那么最好做应用的优雅重启,避免在部署阶段导致服务中断,而与此同时,还需要改造 deploy.sh 以支持优雅重启。

小结

在这一篇文章讲解了,如何利用 bat 脚本、ssh 命令来部署我们的 golang 应用程序。

其实在工作中,我真的有遇到过这种发布模式,很简单,几行脚本就搞定了大套的发布流程,看起来很作坊,很不成体系,但是确实成本最低的实现方案。

在下一章中我们将开始利用 Docker 来部署我们的项目。

更新日志

  • 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. 没有找到日志记录在哪里,需要在看一下。