Redis 平时用的挺多,大多是情况用于队列消费和缓存,很少用上事务,最近因为队列高并发消费问题,处理完成后整理出相关的信息;

背景

大量业务需要从 Redis List Pop 出任务进行处理,部分任务处理可能出现因为第三方接口频率限制或者异常等情况,需要进行重试。那在高并发消费的情况下就可能出现「从redis pop后的任务因为tomcat重启等各种异常丢失」因此我们需要一个事务,当消费失败以后,回滚任务;

问题

Redis有MULTI事务,但实际上是Pineline模式,中途是无法获取到返回值的做下一步业务逻辑判断的,同时担心Pop出的任务因为消费时间长,服务重启等干扰因素,需要一个类似 Kafka 的消费完成后删除任务的机制;

方案一

可以利用LPOPRPUSH这样的命令进行操作,丢到一个备用队列,消费完成去备用队列删除,完成一次类似于「Commit」操作,此方案有几个问题

  1. 利用LPOPRPUSH无法更改 List Value 的值,也就是List信息没法修改,并不知道这个Value的业务属性,比如更新一个任务有效时间;
  2. LREM 操作 List 删除值在List长度特别长的时候计算复杂度非常之高,并发高时特别消耗Redis CPU;

方案二

因为Redis事务支持的是pineline模式,无法获取返回值的,其实不满足原子性,为了避免这样的问题,需要引入 Lua 脚本,由于 Lua 脚本是提交到 Redis server 进行一次性执行的,整个执行过程中不会被其他任何工作打断,其它任何脚本或者命令都无法执行,也就不会引起竞争条件,从而本身就实现了事务的原子性。

那么我们可以利用 LuaScript 实现pop出队列的值,进入以当前时间戳为Score的ZSet,若30秒内为完成任务作业,我们利用同样的方式把 ZSet 中的 Score 超时任务还原回原来的队列;

    private static final DefaultRedisScript<List<String>> leftPopAndAddIntoZSetRedisScript = new DefaultRedisScript<>("" +
            "local values = {}; " +
            "for i = 1, ARGV[1], 1 do " +
            "   local value = redis.call('LPOP', KEYS[1]); " +
            "   if not value then " +
            "       return values; " +
            "   end; " +
            "   redis.call('ZADD', KEYS[2], ARGV[2], value); " +
            "   table.insert(values, value); " +
            "end; " +
            "return values;", (Class<List<String>>) Arrays.<String>asList().getClass());

    private static final DefaultRedisScript<Long> popZSetAndRightPushRedisScript = new DefaultRedisScript<>("" +
            "local set = redis.call('ZRANGEBYSCORE', KEYS[1], '0', ARGV[1]); " +
            "for k, v in pairs(set) do " +
            "    redis.call('ZREM', KEYS[1], v); " +
            "    redis.call('RPUSH', KEYS[2], v); " +
            "end; " +
            "return table.getn(set); ", Long.class);

    public String leftPopAndAddIntoZSet(String listKey, String zSetKey) {
        List<String> values = leftPopAndAddIntoZSet(listKey, zSetKey, 1);
        return (values.isEmpty()) ? null : values.get(0);
    }

    public List<String> leftPopAndAddIntoZSet(String listKey, String zSetKey, int sizes) {
        return stringRedisTemplate.execute(leftPopAndAddIntoZSetRedisScript, Arrays.asList(listKey, zSetKey), String.format("%s", sizes), String.format("%s", Instant.now(clock).toEpochMilli()));
    }

    public long popZSetAndRightPush(String zSetKey, String listKey, long maxScore) {
        return stringRedisTemplate.execute(popZSetAndRightPushRedisScript, Arrays.asList(zSetKey, listKey), String.format("%s", maxScore));
    }

Ngrok如果你完全不知道它是什么东西,可以在它的github项目上了解下:https://github.com/inconshreveable/ngrok

简单概括:能够将你本机的HTTP服务或TCP服务,通过部署有ngrok服务的外网伺服器暴露给外网访问,网上所说的内网穿透

之前的几个月里本人一直用官方订阅版本的ngrok,之所以用付费模式,主要因为免费模式每次都生成一个随机的二级域名,让人很不舒服,但实际几个月下来的使用情况,评估发现总体用的频率并不是很高,同时网络效果也不是很好(应该和墙有关),同时也尝试了一下国内小米球Ngrok,同样有一些不稳定因素,所以打算自己编译一个私有的Ngrok,在编译过程中发现了不少问题,重新整理一遍文档。

1. 安装go语言开发环境/Git

ngrok是利用go语言开发的,所以先要在服务器上安装go语言开发环境。

yum install golang

~/.bash_profile文件内,加入以下环境变量配置内容:

export GOPATH=$HOME/go
PATH=$PATH:$HOME/.local/bin:$HOME/bin:$GOPATH/bin

最后保证可通过go env查看是否配置成功。

安装Git方式略过,这里没有对版本有要求,我们只是利用GIt获取Ngrok源码,不用Git直接Wget github zip文件也是可以的。

3. 获取ngrok的源码

由于下面编译过程需要改官方的部分源码,同时这里也测试过官方最新的版本总是存在「remote error: bad certificate」问题(文末有描述)。这里用的是mamboer的源码基于1.7.1版本,应该有做一些变更,所以尽可能Fork一份源码至自己的Github账户。

cd /usr/local
git clone https://github.com/mamboer/ngrok.git

源码拉取下来后,看情况需要修改一个地方: 打开 src/ngrok/log/logger.go 文件 将 code.google.com/p/log4go 修改为 github.com/alecthomas/log4go 这里主要考虑 code.google.com 已经下线,其实不修改也可以。

4. 生成自签名证书

使用ngrok.com官方服务时,我们使用的是官方的SSL证书。

自建ngrokd服务,证书这里也可以用Godaddy付费证书/Let’ Encrypt免费证书,不过出于个人测试或者沙盒的要求,就不用第三方证书。所以我们需要生成自己的自签名证书,并编译一个携带该证书的ngrok客户端。

证书生成过程需要一个「基础域名」。 以ngrok官方随机生成的地址”1iedks2cv.ngrok.com”为例,其「基础域名」就是”ngrok.com”,如果你要 提供服务的地址为”example.ngrok.xxx.com”,那「基础域名」就应该 是”ngrok.xxx.com”。

我们这里以「基础域名」=”ngrok.crackedzone.com”为例,因为我将来希望建立”pay.ngrok.crackedzone.com”用于平台支付回调的穿透模式。

在此之前我们给域名”crackedzone.com”添加两条A记录:”ngrok.crackedzone.com”和”*.ngrok.crackedzone.com”,指向这台Ngrok所在的服务器ip。

同时在服务器上生成证书的命令如下:

cd ngrok
openssl genrsa -out rootCA.key 2048
openssl req -x509 -new -nodes -key rootCA.key -subj "/CN=ngrok.crackedzone.com" -days 5000 -out rootCA.pem
openssl genrsa -out device.key 2048
openssl req -new -key device.key -subj "/CN=ngrok.crackedzone.com" -out device.csr
openssl x509 -req -in device.csr -CA rootCA.pem -CAkey rootCA.key -CAcreateserial -out device.crt -days 5000

执行完以上命令,在ngrok目录下就会新生成6个文件:

-rw-rw-r-- 1 lv lv  985 Feb 24 19:04 device.crt
-rw-rw-r-- 1 lv lv  895 Feb 24 19:04 device.csr
-rw-rw-r-- 1 lv lv 1679 Feb 24 19:03 device.key
-rw-rw-r-- 1 lv lv 1675 Feb 24 19:01 rootCA.key
-rw-rw-r-- 1 lv lv 1103 Feb 24 19:03 rootCA.pem
-rw-rw-r-- 1 lv lv   17 Feb 24 19:04 rootCA.srl

ngrok 编译过程会加载 assets/client/tls, assets/server/tls 这里的证书资源文件,替换成自己生成的。

rm -f assets/client/tls/ngrokroot.crt
rm -f assets/server/tls/snakeoil.crt
rm -f assets/server/tls/snakeoil.key

cp rootCA.pem assets/client/tls/ngrokroot.crt
cp device.crt assets/server/tls/snakeoil.crt
cp device.key assets/server/tls/snakeoil.key

5. 编译客户端ngrok和服务端ngrokd

make release-server release-client

成功编译后,会在bin目录下找到ngrokd和ngrok这两个文件。

ngrok可以下载到内网的Client服务器上,存放在/usr/local/bin/ngrok

6. 服务端运行ngrokd服务

nohup /usr/local/ngrok/bin/ngrokd -domain="ngrok.crackedzone.com" -httpAddr=":80" > /tmp/ngrok.log 2>&1 &

有需要可以编写一个服务文件,同时记得开放防火墙80端口和4443端,如果证书需要包括443端口。

7. 客户端运行ngrok

创建一个配置文件: ngrok.yml

server_addr: "ngrok.crackedzone.com:4443"
trust_host_root_certs: false

tunnels:
  pay:
    remote_port: 80
    subdomain: pay
    proto:
      http: 127.0.0.1:10780

启动客户端

/usr/local/bin/ngrok -config=/root/ngrok.yml start pay

注意事项

客户端”ngrok.yml”中”server_addr”后的值必须严格与服务端-domain以及证书中的「基础域名」相同,否则Server端就会出现如下错误日志:

[02/24/20 09:55:46] [INFO] [tun:15dd7522] New connection from 27.12.33.23:3994
[02/24/20 09:55:46] [DEBG] [tun:15dd7522] Waiting to read message
[02/24/20 09:55:46] [WARN] [tun:15dd7522] Failed to read message: remote error: bad certificate
[02/24/20 09:55:46] [DEBG] [tun:15dd7522] Closing

头一次接触ORID方法在2015年的一次敏捷回顾上,但是还不知道它,当天围坐在小会议室中通过该方法总结迭代开发经验,使我很有收获。

ORID工作法很好理解,几乎一听就会,只是日常工作中我们总会选择更偷懒的方法,而忽略了总结过程中思考和逻辑的重要性。根据百度百科,ORID是一种通过催化师引导来开展的结构化汇谈(会议、交谈)形式。该方法常被用作对事实进行分析和感觉某一工具和方法。

通过这个方法,在两年后我自己主导转型敏捷迭代开发中再次利用到,可以设计引导结构:

  • Objective: 上个迭代有哪些让你印象深刻的事情发生?你看到了什么?
  • Reflective:哪些场景让你兴奋?哪些地方不那么顺利?
  • Interpretive:为什么会不顺利?这些数据使你意识到了什么?我们如何才能做得更好?
  • Decisional:什么是下个迭代我们可以立刻开始动手的?

此次通过该引导,我得到这样的结论:

回顾

Objective

  1. 团队很和谐,研发节奏很好。
  2. 项目每到上线发布时刻,很紧急。
  3. 便利贴对工作帮助很大。
  4. 对功能细节的评审缺乏。
  5. 产品有太多的优化空间。

Reflective

  1. 工作交流比较开心。
  2. 虽然加班,但是也很有劲。
  3. 看到产品没有人使用,不开心。
  4. 看不到决策层反馈,比较迷茫。

Interpretive

  1. 每天的计划可以做的更细致。
  2. 研发过程中还是需要多沟通。
  3. 定期的传达决策层的建议。
  4. 需要有更好的用户体验。
  5. 团队需要保持。
  6. 需要团建。

Decisional

  1. 希望研发团队能有经费做一次聚餐
  2. 希望看到领导层面对这个项目的期望以及信息的传达
  3. 希望针对这个项目招聘一个推广运营
  4. 每一个研发迭代需求文档提早一个迭代期出来
  5. 每个便利贴都有一个负责人标示在便利贴上
  6. 尽量在周二上午就提交这个一个迭代的功能验收测试版本
  7. 每天站会细致的定义今天的工作必须完成的内容
  8. 迭代的需求评审会议议定要订出本次迭代的功能目标

将Decisional作为下一个迭代要去执行的工作,如此看来相当有难度,说明我的团队管理还是做得不够。

1. 启动网卡

ifup eth0

2. SSH链接 ifconfig 查看IP后SSH终端连接

3. 更新基本软件包

yum install ntpdate wget lsof -y 

备份原系统更新源

mv /etc/yum.repos.d/CentOS-Base.repo /etc/yum.repos.d/CentOS-Base.repo.backup

进入yum.repos.d目录

cd /etc/yum.repos.d
# 下载网易镜像源:
wget http://mirrors.163.com/.help/CentOS6-Base-163.repo
# 或者
# 下载搜狐镜像源:
wget http://mirrors.sohu.com/help/CentOS-Base-sohu.repo

更新系统以及内核

yum clean all
yum makecache
yum upgrade

4. 系统时间更新和设定定时任务

echo '*/30 *  *  *  *  /usr/sbin/ntpdate cn.pool.ntp.org && hwclock -w && hwclock --systohc >/dev/null 2>&1' >> /var/spool/cron/root

5. 修改ip地址、网关、主机名、DNS #eth0 网卡设置

mv /etc/sysconfig/network-scripts/ifcfg-eth0 /etc/sysconfig/network-scripts/ifcfg-eth0.bak
vi /etc/sysconfig/network-scripts/ifcfg-eth0
DEVICE=eth0 #网卡设备名称
HWADDR=00:0C:29:D0:C7:B5 #以太网设备的对应的物理地址
TYPE=Ethernet #网络类型为以太网模式
UUID=080a457b-6a53-4a3a-9155-a23c1146c2c6 #通用唯一识别码
ONBOOT=yes #是否启动引导的时候激活YES
NM_CONTROLLED=no #设备eth0是否可以由Network Manager图形管理工具托管
BOOTPROTO=dhcp #静态IP地址获取状态 如:DHCP表示自动获取IP地址
IPADDR=192.168.1.10 #IP
IPV6INIT=no
IPV6_AUTOCONF=no
NETMASK=255.255.255.0 #网卡对应的网络掩码
GATEWAY=192.168.1.1 #网关地址

网关配置

vi /etc/sysconfig/network
#表示系统是否使用网络,一般设置为yes。如果设为no,则不能使用网络,而且很多系统服务程序将无法启动
NETWORKING=yes
#设置本机的主机名,这里设置的主机名要和/etc/hosts中设置的主机名对应
HOSTNAME=c65mini.localdomain
#设置本机连接的网关的IP地址。例如,网关为10.0.0.1或者192.168.1.1
GATEWAY=192.168.1.1

DNS

vi /etc/resolv.conf
; generated by /sbin/dhclient-script
nameserver 8.8.8.8
nameserver 4.4.4.4

HOSTS

vi /etc/hosts
127.0.0.1 lvtao.localdomain
#使用DNS域名服务器来解析名字
order bind hosts
#一台主机是否存在多个IP
multi on
#如果用逆向解析找出与指定的地址匹配的主机名,对返回的地址进行解析以确认它确实与您查询的地址相配。为了防止“骗取”IP地址
nospoof on

重启网卡

service network restart

6. 修改SSH端口号和屏蔽root账号远程登陆

#备份SSH配置 
cp /etc/ssh/sshd_config sshd_config_bak 
#修改SSH安全配置 
vi /etc/ssh/sshd_config
#SSH链接默认端口
port 52113
#禁止root账号登陆
PermitRootLogin no
#禁止空密码
PermitEmptyPasswords no
#不使用DNS
UseDNS no

重启服务确认端口生效

/etc/init.d/sshd reload 
lsof -i tcp:52113

7. 锁定关键文件系统

chattr +i /etc/passwd
chattr +i /etc/inittab
chattr +i /etc/group
chattr +i /etc/shadow
chattr +i /etc/gshadow

8. 精简开机自启动服务

#关闭全部服务
for sun in `chkconfig --list|grep 3:on|awk '{print $1}'`;do chkconfig --level 3 $sun off;done
#开启需要的服务
for sun in crond rsyslog sshd network;do chkconfig --level 3 $sun on;done
#或者需要使用防火墙的话可以开启iptables和ip6tables
for sun in crond rsyslog sshd network iptables ip6tables;do chkconfig --level 3 $sun on;done
> chkconfig --list|grep 3:on
crond           0:off   1:off   2:on    3:on    4:on    5:on    6:off
ip6tables       0:off   1:off   2:on    3:on    4:on    5:on    6:off
iptables        0:off   1:off   2:on    3:on    4:on    5:on    6:off
network         0:off   1:off   2:on    3:on    4:on    5:on    6:off
rsyslog         0:off   1:off   2:on    3:on    4:on    5:on    6:off
sshd            0:off   1:off   2:on    3:on    4:on    5:on    6:off

9. 调整文件描述符大小

设置配置,重启生效

echo '* soft nofile 65535 
* hard nofile 65535 
* soft nproc 65535 
* hard nproc 65535 
* soft nofile 65535
* hard nofile 65535' >> /etc/security/limits.conf 

临时生效

ulimit -SHn 65535
ulimit -s 65535

10. 设置系统字符集,使用英文

sed -i 's#LANG="zh_CN.*"#LANG="en_US.UTF-8"#' /etc/sysconfig/i18n

11. 内核参数优化 vi /etc/sysctl.conf

mv /etc/sysctl.conf /etc/sysctl.conf.default
echo 'net.ipv4.tcp_max_syn_backlog = 65536
net.core.netdev_max_backlog = 32768
net.core.somaxconn = 32768

net.core.wmem_default = 8388608
net.core.rmem_default = 8388608
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216

net.ipv4.tcp_timestamps = 0
net.ipv4.tcp_synack_retries = 2
net.ipv4.tcp_syn_retries = 2

net.ipv4.tcp_tw_recycle = 1
#net.ipv4.tcp_tw_len = 1
net.ipv4.tcp_tw_reuse = 1

net.ipv4.tcp_mem = 94500000 915000000 927000000
net.ipv4.tcp_max_orphans = 3276800

#net.ipv4.tcp_fin_timeout = 30
#net.ipv4.tcp_keepalive_time = 120
net.ipv4.ip_local_port_range = 1024 65535

net.nf_conntrack_max = 25000000
net.netfilter.nf_conntrack_max = 25000000
net.netfilter.nf_conntrack_tcp_timeout_established = 180
net.netfilter.nf_conntrack_tcp_timeout_time_wait = 120
net.netfilter.nf_conntrack_tcp_timeout_close_wait = 60
net.netfilter.nf_conntrack_tcp_timeout_fin_wait = 120' > /etc/sysctl.conf

### 生效
sysctl -p

12. 删除不必要的系统用户和群组

#删除不必要的用户
userdel adm
userdel lp
userdel sync
userdel shutdown
userdel halt
userdel news
userdel uucp
userdel operator
userdel games
userdel gopher
userdel ftp
#删除不必要的群组
groupdel adm
groupdel lp
groupdel news
groupdel uucp
groupdel games
groupdel dip
groupdel pppusers

13. 设置一些全局变量

#设置自动退出终端,防止非法关闭ssh客户端造成登录进程过多,可以设置大一些,单位为秒
echo "TMOUT=3600">> /etc/profile
#历史命令记录数量设置为10条
sed -i "s/HISTSIZE=1000/HISTSIZE=10/" /etc/profile
#立即生效
source /etc/profile

由于公司搬迁到原因,一直以来都和其他部门公用SVN,权限分配也是分管部门配置的,SVN上很多项目文件,各种branches,tags命名不规范化,产品运营也都混合使用,介于此情况下,考虑将我们有价值以及正在研发都项目都迁移到我们自己到Git服务上,综合考虑决定搭建一个GitLab。

部署GitLab

部署本身很简单,官方有简单到安装命令步骤

yum install curl policycoreutils openssh-server openssh-clients
yum install postfix
curl -sS https://packages.gitlab.com/install/repositories/gitlab/gitlab-ce/script.rpm.sh | sudo bash
yum install gitlab-ce
yum install lokkit
lokkit -s http -s ssh

安装完先打开 /etc/gitlab/gitlab.rb 修改几个简单到配置,域名/备份路径/仓库路径(保证有充足到磁盘空间)

external_url 'http://gitlab.mydomain.com'
gitlab_rails['backup_path'] = "/home/git/backups"
git_data_dirs({"default" => { "path" => "/home/git" }})

初始化配置

gitlab-ctl reconfigure

重启GitLab服务

gitlab-ctl restart

设置Crontab备份操作

  0  2  *  *  * /usr/bin/gitlab-rake gitlab:backup:create

之后访问平台WEB操作,重置管理员密码,创建用户,组,项目信息

一切就绪,我们需要开始导入原来SVN上需要到项目,这里需要操作时支持git svn命令

迁移

建立users.txt(存储svn账号与gitlab上账号的关联性)

格式: svn用户名 = git用户名,如:

yonghe = yonghe<×××@163.com>  
lihe = lihe<×××@163.com>  

注意: svn里面有的账号必须要做关联,否则clone会失败。比如上面的user11找不到是哪个开发人员,也不知道它该对应哪个git账号,那就随便指定一个git账号就行了,这样做的目的其实就是将user11在svn里面的所有提交日志关联到yqdong的git账号下。转到git之后,原svn账号就无关紧要,各司其职了。

绑定 users.txt 的意义仅在于,将svn里面的提交者日志,注意是提交者,不是svn里面所有的用户信息都得关联,仅仅是往项目提交过代码的svn账号,将这些svn账号找出来,然后关联到现有的git账号,一旦转到git上之后,每个git用户就能看到自己过去在svn里面提交的日志,绕了这么大一圈,现在明白了吧?

可以通过以下命令快速创建 users.txt

svn log -q | awk -F '|' '/^r/ {sub("^ ", "", $2); sub(" $", "", $2); print $2" = "$2" <"$2">"}' | sort -u > users.txt

导出svn上到项目数据

git svn clone https://svn.mydomain/amz/selenium-center/ --trunk="trunk" --tags="tags" --branches="branches" --authors-file=amz-selenium-center/users.txt --no-metadata selenium-center

添加GitLab到Git源信息

git remote add origin [email protected]:amz/selenium-server.git

将trunk, branches等上传

git push origin master

回到GitLab平台既可看到项目相关Commit信息

更多参考文档:实际操作 Svn 迁移到 Git