stuff(winkyy~

I have but one purpose in this life, seeking the nature of the world.

0%

Linux磁盘占用问题排查

这几天生产服务器硬盘空间满了,服务老是挂掉。问了下老同事,一直有这个问题。

基本上生产服务器不会有什么别的东西,一般就是Nginx日志文件默认配置,长期用会吃掉一些空间,但是是可以手动配置最大日志占用空间的;然后可疑的地方大致就是docker。

快速定位问题并暂时修复

所以,总之先df -h看下磁盘占用,果然某overlay占用上天了。赶紧先docker rm掉一些旧的image,虽然只空出来2g左右的空间,但是还是能先把生产服务稳定了。

大体上,这边CICD是配合自建gitlab+自建docker registry,通过swarm来管理的,每一份image都会在生产环境下拉取,再写个定时任务、清理掉2天前的image。

看起来问题不大(除非2天内疯狂发几百个版)(遗憾的是,按照这里的开发规范,这种事情理论上的可行的),所以这里暂时不管了。如果后续要改进,就要考虑权限管理问题,然而讲道理gitlab本身就是有权限控制的啊,所以这本质上……

回来再看看磁盘占用还有什么可以改的

1
docker system df -v

通过命令查看image、container和volume的具体占用情况。

image占用异常的情况,可能就是上述的,不打tag而选择定时任务清理,配合开发流程上的缺陷,这些事件共同促成的。

contanier非常轻量化,很多都是以b为单位的,基本不会出现问题。

volume常见的也是日志文件,或者使用容器时不指定卷名,导致群魔乱舞,又或者甚至是数据库文件(茶)。

没错我看到了一个19gb的volume,明晃晃写着prod_redis……自建redis,开了aof持久化,然而并没有写自动重建,所以aof稳定上升,直到今天。(今天又学到了一种超隐蔽的延时bug了呢!以后接外包不结尾款……)

话不多说,直接讲解决方案:

首先

1
docker system prune -a

清理掉无用的image和container

然后ssh或者docker attach进到redis服务里面

1
redis-cli bgaofrewrite

重建aof文件,整个重建过程会先生成新的aof文件,再删除旧的,因此如果本身一点空间都没有了,是没法重建的,就只能先加硬盘了。手动跑一遍的意义在于确认剩余空间是否足够,并且重置自动重建的阈值。自动重建是根据上一次重建的文件大小的比例来确认重建时机的,因此像现在这么大的aof文件,即使打开自动重建也会达不到阈值无法生效。

最后修改redis启动命令

redis-server
–appendonly
yes
–auto-aof-rewrite-percentage
200
–auto-aof-rewrite-min-size
5gb

设置合适的重建时机。这个本身volume没有命名,问题不大,毕竟redis也不太会更新。

应用无状态

有对外访问的业务,公司自己搭了一套代理服务换IP。处理业务问题时,发现即使是同一个用户,连续的访问,代理IP也在一直变。就检查了代码,发现了一些问题。主要提一下应用无状态这个设计思想。

why and how

以这边的业务为例,需要维护一个可用的IP池,设计上应该尽量满足均衡使用,IP不可用时换新或者等待再次可用。均衡使用和无状态没多大关系不多提,这里就只关心IP可用性的维护。

一个很简单的做法就是,直接新定义一个数组,数组内的元素就是一个IP对象,给每个IP对象一个属性标记是否可用。这么做就是一个有状态的应用,每个IP的状态都保留在这个活动应用实例内。这么做是部分OK的,如果业务量不大,并且是单实例运行的话。

实际上目前应用开发普遍都用到了集群(听说达内都开始教docker了XD)。为了保证服务的高可用,一般生产环境都会通过k8s或者swarm配置多个应用副本,既可以分流,又可以保证在部分副本出现问题时,仍有副本可以正常提供服务(同时等待集群启动新的替换副本)。

因此,如果设计了有状态的应用,一个最大的问题就是,同一个应用的不同副本之间,状态是不方便直接沟通的。就如这个例子里,实际上是生成了双副本,那么用户访问时,第一次访问到了实例A,发现IP被封了,实例A就把自己维护的IP状态改成不可用;用户第二次访问时,如果再次访问到实例A,那么这个失效IP不会给用户,是正确的,如果访问到实例B,实例B并不知道这个IP已经失效,仍然给到了这个IP,就是错误的,没有起到维护可用IP池的功能了。而这个分流是随机的,如果没有提前考虑好,后续随机出现问题,排查起来还是很不方便的。

现在常用的方案是,需要保存的状态存放在redis内;应用保持无状态,需要的时候就去redis读写。

当然后续也有高并发下状态一致性的问题,解决方案也是根据具体业务需求来的,这一业务只要求大致准确、失败时最终检出,也就没有加锁,直接先读判断,bool取反,优先改成false即可。其他业务就看自己需求,这里不展开了喔。

对比

因业务需要赶紧啃一手MongoDB,大致过了一遍,对比主流关系型数据库,MongoDB主要有这些不同:

  • 列是动态的,更适合存储不定属性的对象(如某些不同业务的聚合统计);
  • 原生支持js方法调用形式的写法,当然坏处是相对的,只能背API,没办法用通用的sql方法;
  • 不支持事务、回滚,这也限定了MongoDB不适合强一致性的场景(基本只用来存插入的业务记录好了,更新都不要想了),没有join,不适合业务复杂的场景;
  • 贵(特指某里云),好在服务可以自建,不考虑高可用的情况下负担可以接受;
  • 其他特性和不同我暂时考虑不到。

简单使用

怎么直接就入了土呢?我的业务主要用到一个MapReduce,一个find,一个upsert。然后结合实际的使用,总结了一些坑点:

  • 请用最新的包(目前是3.3.2),不要用2.x的包。一个是内部环境的问题,这个后面会和MapReduce的坑一起讲;再一个是解析问题,connect的时候新包会提示你打开使用新解析方式的选项,而且后续会废弃掉旧的解析方式,所以旧包应该是有这方面问题的。
  • 可视化工具win下用的是Robo 3T,一个坑点在于如果是通过ssh连跳板机到线上,本地生成sshkey时,需要指定pem的生成模式,否则是连不上的。(sshkey有机会再讲)

具体应用的话,find相当于select;updateOne带一个upsert的参数,这样就相当于sql中的insert on duplicate key,但是可以指定匹配键,而非sql中的索引键;这些是比较常用且比较基础的用法。

mapReduce

比较重要的就是这个MapReduce了,主要就是用作数据分析:map的过程根据一定条件将原始数据映射到不同的集合(集合内只有一个元素时不走reduce,因此设计返回值时,map的结构应该尽量和reduce保持一致),reduce过程将每个集合按一定方式处理成想要得到的分析维度。举一个简单的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
db.collection('test').mapReduce(function(){
emit(this.id % 10, {
count: 1,
avgScore: this.score
})
}, function(key, values){
const res = {
count: 0,
sum: 0,
avgScore: 0
}
values.forEach(value => {
res.count += value.count
res.sum += value.score
res.avgScore += value.score
})
res.avgScore /= values.length
return res
}, {})

这一段,map部分,将test表的所有元素按id模10的结果分组;reduce部分,计算了每个组内成绩的平均值;我这里假设样本足够均匀,id从0到9都有结尾的,那么最终的结果就是一个这样的数组:

1
2
[{key: 0, value: { count: 1, avgScore: 20 }}, {key: 1, value: { count: 4, sum: 216, avgScore: 54 }}, ...{key: 9, value: { count: 5, sum: 195, avgScore: 39 }}] // length: 10

可以注意到,生成的结果是一个列表,其中每一个元素都是一个对象,都有key和value两个属性。key是在map中指定的,emit的第一个参数;value则是reduce的返回值,如果map映射到的只有一个数据,那么emit的第二个参数直接作为结果对象的value属性(如例子中的第一个结果,只匹配到一个数据,因此不走reduce,也就没有sum属性)。
这里注意到map函数内有一个this.id,这个id是怎么this出来的?为什么在map函数里面console.log会报错?为什么不能传参数进去?
都是因为:map,reduce这两个部分的函数,是作为参数传进mapReduce函数里面的,传入后会toString掉,在MongoDB内部环境执行。这也是为什么上一节说到,要使用高版本的npm包,因为2.x版本的包只支持到es5的语法,如果一定要用老包的话,代码会写得很难看(然后被自己嫌弃(误,明明是被cr掉
如果我真的要传参数进去呢?确实是可以的。传入的参数可以是函数,也可以是字符串,所以……

1
2
3
4
5
6
7
8
9
function map (options) {
return function temp () {
const options = JSON.parse('$options')
switch (options) {
// ...
}
emit(key, value)
}.toString().replace('$options', JSON.stringify(options))
}

所以就直接传字符串好了,当然也还是有一些限制,没办法用全局变量,没办法传入构成循环指向的对象,还是没有console……

要问到底为什么入土

就是效率比较低,当然也有可能和我没有建好索引,或者map、reduce函数写得不好有关系。目前的话,大概100k左右的数据量就会崩掉。10k左右的数据量,CPU单核会跑满。
确实优点还是有,insert on duplicate key不需要提前建立索引,不需要考虑字段类型,容量扩展较方便,mapReduce甚至可以直接写js,虽然有些鸡肋,但是心智负担确实比写sql的group by稍稍低一些……呸,我有这时间折腾这玩意,业务早写完了……
(完)

因业务需要,要做某网站的模拟登陆,有个比较简单的图片验证码。

之前登陆的话,同事用的基本都是js逆向。然而已经是前同事了,然而逆向的工作量我是不愿意接受的,所以划水的时候做了一个小demo,尝试解决这个问题。

当然了网站名字不好透露,这里放一部分测试数据。

image

image

image

image

image

image

思路:

首先看下商业上是怎么处理图片验证码的,借鉴一下。

百度图像识别,图像文本识别等关键词,出现了ali和tx的广告(打钱),有一个关键词频繁出现:OCR;于是又搜了一下OCR,OCR (Optical Character Recognition,光学字符识别),这个应该算是比较具体的技术名字了;拿这个关键词去GitHub搜一下,果然有现成的东西

下载下来好好康康,C++写的,win/linux都能用,直接用cli操作,指定输出路径。那这就很勇了,安装走起。

自己用画图手写了几个算式,识别效果还不错。然而直接用目标网站的图片就凉凉了……

这里简单分析了一下,应该是图片内容有问题,可以看上面的测试数据集,多了一部分干扰线;再一个tesseract的设置应该也有问题,虽然什么都没有识别出来,但是结果集是一个四行的空文档。后者直接看tesseract的文档,有一个配置项–psm,是预指定的识别模式,看了一下选择13,单行识别模式。前者的话,这一版是暂时做了简单的图像处理。

那么整体的处理模块就分为两个部分了:第一个,图像去干扰;第二个,调包生成结构再处理;

一、图像去干扰

这个图片集本身是比较简单的(这也是我做着玩也能做出来的原因,笑,不过思路还是可以分享一下)

分析一下图片,第一,图片四周一圈单像素的干扰信息,看上去就是一个黑框;第二,图片中的干扰像素和信息像素颜色区分度还算是比较高的,信息像素基本是偏向黑色的,后期查看虽然不是完全的#000000,rgb值都确实是普遍小于干扰像素的。那么就设置一个阈值,筛选出不是那么黑的像素点,直接涂白就行了。

具体代码如下:

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
const getPixels = require('get-pixels') // 这个包比较老,只能用回调,promisify都救不回来

const fs = require('fs')

const jpeg = require('jpeg-js') // 只有getPixels,没有setPixels。手动反向操作加密图片内容后再输出

const { exec } = require('child_process')

const detach = 30 // 颜色阈值,一边试一边调,手动调到一个合适的水平

let pic = 'math'

process.argv[2] && (pic += process.argv[2])

const solve = () => {

  getPixels(`${pic}.jpeg`, (err, pixels) => {

    if (err) {

      return

    }

    let x = pixels.shape[0]

    let y = pixels.shape[1]

    for (let i = 0 ; i < x ; i++) {

      for (let j = 0 ; j < y ; j++) {

        let r = pixels.get(i, j, 0)

        let g = pixels.get(i, j, 1)

        let b = pixels.get(i, j, 2)

        // let a = pixels.get(i, j, 3)

        if (r > detach || g > detach || b > detach || i === 0 || j === 0 || i === (x - 1) || j === (y - 1)) {

          pixels.set(i, j, 0, 255)

          pixels.set(i, j, 1, 255)

          pixels.set(i, j, 2, 255)

        }

      }

    }

    let temp = {

      width: x,

      height: y,

      data: pixels.data

    }

    fs.writeFileSync('./out.jpeg', jpeg.encode(temp).data)

   // orc() // 调包处理

  })

}

去干扰前后对比

image

image

二、调包处理

这个没啥好说的,windows下的安装包下载地址

这个一路点点点下一步就好,安装完需要手动设置环境变量,环境变量不会设置的百度一下吧……

会linux的就不用说了吧……

对应代码如下:

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
29
30
31
32
33
34
const orc = () => {

  exec('tesseract out.jpeg out --psm 13', () => {

    const s = fs.readFileSync('./out.txt').toString('utf8')

    let res

    let nums = s.match(/\d+/g)

    if (s.match(/\+/)) {

      res = Number(nums[0]) + Number(nums[1])

    } else if (s.match(/-/)) {

      res = Number(nums[0]) - Number(nums[1])

    } else if (s.match(/x/)) {

      res = Number(nums[0]) * Number(nums[1])

    } else if (s.match(/÷/)) {

      res = Number(nums[0]) / Number(nums[1])

    }

    console.log(res)

  })

}

差不多就是这样了,试一试,发现了两个问题,乘号被识别为了小写字母字符x,这个将就一下也可以用;除号基本识别不出来,这个就不是三分钟能解决的问题了。好在这个东西识别错了也没什么大问题,再调一次接口就好了。

所以既然这一期的标题里有一个“上”字,那这篇文章就讲到这里了;下一期划水的时候,我想尝试一下tesseract训练集,专门处理一下乘号、除号无法识别的问题。

简介

我都不知道这篇文章到底该叫什么XD了,主要是总结了在前几个月里做的一些工作:

做了一个crud功能模块但是这里不讲;做了一个竞价排名系统,但是重点甚至不在其本身,而在于改进旧项目;做了一部分项目重构。

旧项目

竞价需求

需要排名的是1000+个对象(后续需要能实时添加),在未竞价时本身有一套排序体系,从多个维度比较,维度有优先级,这里代称为A、B,对象1.A大于对象2.A时,对象1排在前面,如果A相等,再比较B,以此类推。而竞价本身也作为一个维度,优先级最高,但是只选出前N名排在前面,超过N的退钱,并且按未竞价的情况排名。

竞价需要设置每次点击扣费额以及总次数。每次扣费额作为排名依据,总次数点击完了之后不再扣费,同时排名重新按原来的体系排序。竞价成功后,到期没用完的次数直接参与下一轮竞价。

产品原本的要求是竞价后即时排序,点击数用完后即时下撤。经过大TL和小TL的共同努力,在与产品进行友好沟通后,改成了每10分钟统一换一次榜。(竞价本来是大TL的工作XD)

难点

排个序问题本身不大,这个数量级都轮不到自己写排序算法。

还是稍稍介绍一下主项目本身吧。是自研的一个Node 6下的web框架;启动时将一部分不经常改变的数据,外加一些基于这部分数据计算的数据,直接存放在全局变量里,名曰“静态数据”;有很多自定义的全局函数;mysql全局封装,不支持事务;redis全局封装,只能写get和set方法;没有守护进程;没有负载均衡或多副本;一部分数据每月更新,主项目需要冷重启,停服两小时。

一个问题在于属性A、B说着挺简单,但是实际上是不固定的,并且是一个分库的统计字段。修改需要结合另一个项目手动跑脚本改数据库再统计,主项目再冷重启。

再一个10分钟排一次,中间还要可以加价,并且还要保存每次的竞价结果,客户端需要能查看本人的时序结果,后台既要看到单人的时序、又要看到每个时间节点全部的时序,保留至少30天。我这里只写了A、B属性,实际上维度有很多,数据量是一个值得考虑的问题。

解决方案

应用无状态化

考虑到冷启动是经常的,也不应该有所谓“静态数据”这种东西,首先要将“静态数据”放在redis;这些数据一般只在另一个手动脚本运行时才发生改变,因此这些数据统统交给这个脚本维护。这样主项目即时冷重启,也只会读redis里维护好的数据,而非重写读数据库重算。这部分的工作将Node主进程占用900MB的内存降低到了200MB左右,也便于后续做多副本。

之所以仍然要用这套数据,还是因为这套东西过于基础,项目里到处都在用。这么改才具有可行性。后期的话直接重写的。如果没有这种历史包袱的话,预期不会变的数据,比如平台名、项目名,直接写在config.js里;任意预期会改变的东西,不管改得是否频繁,都应该写在数据库里。

守护进程+负载均衡

这个和同事配合着做,主进程用pm2管理;负载均衡直接用阿里云的,切好端口、重新配置好证书即可。后期终于迎来了一位运维小哥哥,感恩。

OSS储存记录

没别的原因,便宜点。只保留30天,就按日覆盖文件夹,时序数据和全局数据直接生成json。要是前端配合的话可以直接在前端做oss权限验证,数据都不走服务器美滋滋(可惜前端框架也是上面同一批人自研的XD)。

重写和一些感想

重写最重要的原因,是这个框架是真的沉疴难愈了……哪来那么多中华田园架构师写自研……

这部分是Node组配合一起做的,我需要将我之前开发的crud模块和一部分相关模块用egg重写。

啊,新鲜完整的mysql的香味,久违了。旧项目支付体系之所以从来不出问题,完全归功于客户量少,没有出现过并发场景。但这不代表一个技术仔内心没有受到煎熬。你(市场)可以不行,我不可以不行。万一哪天付款客户真的多到高并发了呢?

重写的要点在于尽可能按产品说明好好写测试样例,遗憾的是这的确会拖慢很多开发进度。我听过太多次“时间紧,赶赶吧”,在开发质量和效率间,TL和老板选择效率,即时我一再强调,也不曾思考过“后续维护欠债”的后果。

我现在已经从前公司离职了,我认真思考过了,我做得再快一点,产品会有起色吗?

我觉得不行。

老板是销售出身,很会说,也有一些人脉。但是产品是要用真金白银买的,你的产品创造的东西有没有价值,有多大价值,是需要用事实说话,而非花里胡哨数特性优点、攀关系来的。你说你的产品能帮我省钱,帮我赚钱,告诉我有ABCD一大堆优点作为理由,我看的不是ABCD,我看的是到底是不是帮我省钱赚钱。

以上。