一种从客观数据到代码实现的编程思想

这套编程思想,从我日常工作中总结得来,用于解决一些复杂业务问题。内容偏抽象,表达能力有限,不喜请绕道。

一、调研客观数据,放弃主观思考。

以往解决问题,往往基于认知和思考。认知是有限的,主观思考易产生偏见。

解决问题,就从问题本身出发,尽可能搜集问题的表现数据,找出数据特征和规律。最后,程序要的做,就是把数据的特征和规律描述出来。

二、程序是数据的处理过程。

所有程序,最顶层的抽象,都是数据的“输入 – 处理 – 输出”。如下图所示:

实现中,中间的“处理”过程,是程序的主要复杂点。“计算机科学领域的任何问题都可以通过增加一个间接的中间层来解决”。对于复杂的处理,一个中间层不够,那就多个,于是可以得到:

在上面的模型中,每层都封装了数据。一层的数据,转换为下一层数据的过程,就是逻辑。

下面是一个虚构的商品请求程序模型:

这个商品请求程序模型,存在五层数据。第一层的数据是“商品请求数据”,通过逻辑“查询商品”,转换为第二层的数据“商品基础数据”……

程序也可以描述为:输入数据,经过多层转换处理,最终得到输出数据的过程。

三、数据模型和关联。

数据(模型/结构)由属性/字段组成,不带操作/逻辑(逻辑被视为数据的转换过程)。每层有一到多个/块数据。单层内的数据之前可能存在关联,这种关联在代码上表示为数据间的组合。

下面是一组虚拟的用户和钱包数据模型:

上面的数据模型,如果用代码来实现,可能是下面这样的:

type Wallet struct{
    Currency string
    Balance  string
}

type User struct {
    NickName string
    Wallet   *Wallet
    Credit   int
}

四、完整的可实现的程序模型。

一个程序模型示例(省略了数据模型细节):

在上面的示例模型中:存在多道程序逻辑(数据转换逻辑),模块间的边界清晰,部分数据和逻辑被复用,部分数据被复制而不是转换到下一层(实际代码中可能什么也没干)。

下面是一个虚拟的下单请求程序示例(只有一道程序逻辑):

最后要做的,就是用代码,把这个程序模型描述出来。

下面是一个真实案例的模型(省略了大量细节):

上面真实案例模型,是一个邮件发送程序,包含一个邮件发送的通用框架,这个框架支持不同业务(图中的1、2、3)的邮件。

Posted in Uncategorized | Tagged | Leave a comment

新的Go语言缓存中间件:restcache

我开发的第一套缓存中间件是cachex。cachex支持自定义存储后端,实现了哨兵机制,自带lru存储,支持系统故障时使用过期缓存数据。

cachex的主要问题是不支持批量处理。这也导致在上家公司,cachex未能上阵。我在上家三年时间一直不能开发出支持批量处理的缓存中间件,我也写了三年业务耦合的缓存处理代码。

这次的restcachefastrest组件库的一个组件。在写restcache之前,我创建了gox项目,实现了优化版的lru,和一套完整的支持批量处理的哨兵机制,然后再实现restcache就简单多了。解决一个复杂问题的一个有效方法,就是模块拆分。

一套完整的缓存,应该包括三个模块:存储、查询、中间件。三个名字是我取的,不准确。

  • 存储是存储缓存数据的部分,也可以称为后端,一般是redis客户端,或者进程LRU存储。存储逻辑都是透明处理业务数据,也就是存储逻辑是通用的,不用对每个业务逻辑写一套存储代码。
  • 查询是存储查不到目标数据时,提供新缓存数据的部分,一般是从MySQL等持久化数据库或者服务接口查询。很遗憾,查询逻辑是耦合业务数据的,需要对每个缓存业务写一套定制的查询代码。
  • 中间件是响应用户查询,调用存储和查询的胶水逻辑,也就是实现了典型的缓存流程。这个也是通用的。

cachex和restcache实现的就是缓存的中间件,并定义了存储和查询的接口。restcache的用户需要自己实现存储和查询。restcache已经附带了一套lrucache存储,可以直接用。但因为用户的redis库版本会不同,restcache没有实现redis存储。

典型的缓存流程如下:

  • 先查存储。如果找到,返回结果,结束;如果没找到,可能是因为没存或者已经过期,继续下一步。
  • 调用查询,查到新缓存数据,保存到存储,返回结果,结束;如果没查到,返回notfound,结束。
  • 下次查存储,查到上一步保存的新缓存数据。糟糕的情况是notfound,会重复第一、二步。

应该绝大多数缓存都是这个流程。这个流程的主要麻烦有两个:

  • 缓存失效风暴,即大面积缓存失效。缓存本意是避免执行大量重复的低效的查询,但大面积缓存实效会降低缓存的意义,严重的会导致后面的服务失去服务能力。
  • 批量查询。实际场景中,很多列表的查询。批量处理,会让流程中的每个环节变复杂。

解决方案为:

  • 对于缓存实效风暴,cachex和restcache都用上了哨兵机制。我实现的叫SentinelGroup,Go团队实现了一套singleflight。SentinelGroup不见得写得比singleflight好,但强在支持批量处理。两者核心逻辑都是一致,多个过程需要同时取得相同的结果,只放行一个去做生产者,其它作为消费者等待生产者分享结果。
  • 对于批量查询,这是cachex无法实现,但restcache实现了的特性。重复下:解决一个复杂问题的一个有效方法,就是模块拆分。一个复杂模块,由几个小模块简单地构成,每个小模块都封装了一部分复杂逻辑。然后小模块又可以继续拆分为几个更小的模块。cachex的失败,在于没划分清楚缓存和哨兵机制的边界,没能定义出清晰简单的批量存储接口,导致单个模块过于复杂。解决了实现的复杂性,简单来说,缓存的批量处理,就是要从不同的来源查询数据,再把不同来源的数据组装起来。第一步需要从存储和查询中拿到数据,第二步需要从哨兵机制的生产者和消费者拿到数据。因为这两步的边界足够清楚,也就降低了整体的复杂度。

restcache刚刚开发出来,虽然我写了很多单元测试,但经过项目的考验才能成为一个成熟稳定的组件。

Posted in Uncategorized | Tagged | Leave a comment

reloader:Go服务热升级支持

以前我在公司写过一个简单的Go热升级支持程序。但是工作时间写的,领导不准开源。后我想另写一个开源版本,但一直没想到解决多端口的问题。后来就慢慢忘却了。

直到最近一次偶然的机会,这个程序被人重新提起。又遇上待业期,正需要写点东西打发时间。突然灵感来了,想到了多端口的解决方案。

于是有了reloader

思路基本是之前版本的扩展版。一个master进程,多个worker进程,一个worker进程处理一个侦听器。这样可以实现多端口的动态侦听和关闭。我又做了些进程管理的优化,推送到github。

无法否认,我的解决方案存在很多问题,因为偷偷起了多进程。比如程序在所有侦听器和连接都关闭后需要上层逻辑结束,这对于单进程无碍,对于reloader却是可能多了N个垃圾worker进程。这些问题,得等待找到更好的解决方案。

Posted in Uncategorized | Tagged | Leave a comment

重读《黔之驴》

今天计划做点无聊的事。突然想起柳宗元的《黔之驴》。原文如下:

黔無驢,有好事者,船載以入;至則無可用,放之山下。虎見之,龐然大物也,以為神。蔽林間窺之,稍出近之,憖憖然莫相知。他日,驢一鳴,虎大駭遠遁,以為且噬已也,甚恐!然往來視之,覺無異能者,益習其聲,又近出前后,終不敢搏。稍近益狎,蕩倚衝冒。驢不勝怒,蹄之。虎因喜曰:「技止此耳!」因跳踉大闞,斷其喉,盡其肉,乃去。

噫!形之龐也,類有德;聲之宏也,類有能。向不出其技,虎雖猛,疑畏卒不敢取,今若是焉,悲夫!

大意是说:黔地没有驴,一个无聊人运了头驴过去。黔地老虎没见过驴,开始很害怕,后发现驴不过“技止此耳”,跳上去,吃了驴。

故事创作于作者永贞革新失败之后。后人一致认为,作者是在讽刺朝中外强中干的权贵。

以我今天的眼光来看,故事却是这样的:一个很有想法的人,给一个没有驴子的地方运了头驴。驴新来,遭到当地传统势力的敌视,最终不敌,被吃了。这个很有想法的人,就是指作者自己,驴指永贞革新,老虎指当时朝中掌控实权的权贵和地方的藩镇。

我们今天谋求创新,就要做很多看似无聊的尝试,敢于承担失败的风险。想法的落地,则需要短时间适应现实情况,否则很快夭折。即使最终项目失败了,也未必意味着想法是错误的。现在黔地已经没有老虎,留下的是驴。

Posted in Uncategorised | Leave a comment

Python实现数据类的方法集合

当前Python最新稳定版为3.6。主流Linux发行版官方仓库提供Python版本的有2.7和3.5,默认依然是坚强的2.7。

1、dataclass装饰器。正规军,需要Python 3.7+,还在路上。dataclass装饰器会给类添加__init__、__str__、__repr__等方法。

@dataclass
class InventoryItem:
    '''Class for keeping track of an item in inventory.'''
    name: str
    unit_price: float
    quantity_on_hand: int = 0

    def total_cost(self) -> float:
        return self.unit_price * self.quantity_on_hand

2、collections.namedtupletyping.NamedTuple。要求不多的用这两个最合适了。Python 3.6+的typing.NamedTuple才支持默认参数。

class Employee(NamedTuple):
    name: str
    id: int = 3

employee = Employee('Guido')
assert employee.id == 3
Employee = namedtuple('Employee', 'name, age, title, department, paygrade')

3、autoassign装饰器,和auto_assign装饰器。算是奇技淫巧,前者支持Python2,后者支持Python 3.5+。

4、collections.namedtuple + functools.partial。通过partial实现默认参数,也是我当前采用的方案

fields = ("name", "age", "title")
defaults = OrderedDict({"title": None})
Employee = partial(namedtuple("Employee", fields), **defaults)
employee = Employee("Guido", "18")

网上还有一些相对麻烦点的方案,比如重写namedtuple的__new__方法,就不一一列举了

Posted in Uncategorised | Tagged | Leave a comment

在Linux Minecraft中输入中文——进阶:在告示牌输入中文

两年前的一篇文章,我讲到如何在Linux Minecraft中输入中文。脚本内容是转的别人的。该方法只能用于聊天,不能用于编辑告示牌。

其实只要把脚本修改下,就可以通用。只会复制粘贴,不会派生定制,做程序员的我真的不好意思。

原脚本内容为:

#!/bin/bash -e

chars=$(zenity --title 中文输入 --text 中文输入 --width 500 --entry 2>/dev/null)
sleep 0.1
xdotool key --delay 150 Escape t
sleep 0.2
xdotool type --delay 150 "$chars"
xdotool key Return

其中正文第一行为弹出对话框,接收用户输入。第三行输出t键。第五行输出对话框接收的输入。第六行输出回车。很明显,第一行和第五行才是重点。其它几行不过辅助性的,帮用户偷懒。

现在只要改下:

#!/bin/bash -e
 
chars=$(zenity --title 中文输入 --text 中文输入 --width 500 --entry 2>/dev/null)
#sleep 0.1
#xdotool key --delay 150 Escape t
sleep 0.2
xdotool type --delay 150 "$chars"
#xdotool key Return

只要注释掉辅助性的几行。

编辑告示牌时,通过快捷键执行该脚本,即可在告示牌上输入中文。如果是聊天,得先输入t打开聊天栏,再通过快捷键执行该脚本,聊天编辑栏得到中文后,敲回车——或者新脚本和旧脚本共存,绑定到不同的快捷键。

以前我在游戏中结识位朋友。他发现另一个在告示牌输入中文的办法。可惜我还没学会,就与他失去了联系。

Posted in Uncategorised | Tagged | Leave a comment

一次网件路由器OpenWrt系统TFTP刷机抢救记录

路由器网件WNDR4300,原固件为OpenWrt15.05,计划升级到OpenWrt15.05.1。下载了wndr4300-squashfs-sysupgrade.tar,通过LuCI升级。确认升级前,检查升级文件摘要匹配。

路由器重启后,不能获得IP地址,能PING通网关。不能访问luCI,SSH连接网关密码始终无效。路由器在开机后,指示灯只亮电源指示灯和有连接的LAN指示灯。

开始TFTP升级。先下载固件镜像wndr4300-ubi-factory.img。将电脑通过网线连接到路由器LAN接口。电脑网络地址改为手动设置,IP地址为192.168.1.2-254,网关192.168.1.1,子网掩码255.255.255。ping 192.168.1.1,确保可以连接到路由器。

路由器关机,按下RESET键,再开机。先指示灯全亮。等待电源指示灯由黄灯常亮变为黄灯闪烁,再变为绿灯常亮,最后变为绿灯闪烁。路由器进入TFTP刷机模式。松开RESET键。利用TFTP工具将固件镜像上传到路由器。如果是使用Linux的tftp软件上传,需要设置模式为binary。

等待LuCI可以访问,设置ROOT密码,恢复备份存档——前提是你升级前备份了存档!
如果tftp救不了你的机器,准备TTL刷机吧。

Posted in Uncategorised | Tagged | Leave a comment

长沙寻访古旧书店(一)

首先说明,本次寻访日期为2018年1月9日。本次寻访大区域为芙蓉区,寻访依据以网友提供的线索为主。

第一块区域为湘雅附二医院一带。该区域也靠近湖南图书馆。人民路北侧,湘雅附二医院以东,有三家旧书店。三家书店都是小门面,提供二手旧书。我淘到一本人民出版社的《国际共产主义运动史》,1978年版。向老板询价,老板示意书背面。翻过来看,价格在背面用铅笔标注了,13元。

结账时,又向老板打听附近可有其它旧书店,老板告知复兴街还有家。复兴街其实只能算巷子。入口位于附二医院西边。人民路附二医院西边的口子进去不远,可以看到一家“复兴参观”,进入就是。复兴街北接白沙路。穿过林立的小餐馆,进入相对安静的居民区,就可看到一家孤立的旧书店。该店比之前三家店面稍大,卖的也都是旧书。

既然来到白沙路附近,就免不得去天心阁一带。简牍博物馆下面古玩商店不少,然而没有旧书店。询问了老板,才知道要等到周末才有旧书摊。只好作罢。

下一站八一路。从简牍博物馆到八一路,刚好路过定王台,免不得要过去寻访一番。这次没找到定王台书市对面的述古人文书店。发现了一间民生书局。民生书局卖的是出版社库存书。库存书又免不了堆积大量湖南本地出版社的库存书。淘到一本湖南教育出版社的《青藏高原科考访谈录》,2010年版。店家要价25。这次价格没有用铅笔在书背标注。见书品相较好,价格实惠,就立马掏了钱。

IMG_20180109_134850

出了民生书局,再去搜寻述古人文书店,才发现述古已经关了门,招牌上贴上了招租的广告。

最后一站是八一路的星星书店。星星书店位置是袁家岭地铁站北面。大致占据了两个半的小门面。书店以出版社库存书为主。同前面的民生书店,星星书店也不少湖南本地出版社的书,库存书又以湖南科学技术出版社的书为主。淘到《爱因斯坦传》、《西方科学起源》、《居里一家》,三本都是湖南科学技术出版社的书,而且都是全新的,前两本还有原书塑封。店里上豆瓣搜了下,前两本评论都蛮高,后一本评价人数太少,但看介绍也不错,想必不会浪费票子。又搜到一本港版袁伟时的《文化:中国与世界》,木刻文化2010年版,扉页还有作者的签名“丁总赐正 袁伟时 2011.4.27”。丁总显然工作太忙,无暇“赐正”。书一页没翻,就流落到旧书市场。

IMG_20180109_161608

结账时算收银员价格怎么算。收银员告知三本湖南科学技术出版社的书算四折,一本中华书局的《老子注译及评介》算六折,港版的《文化:中国与世界》打了个电话请示后表示30元。果断放弃了《老子》。买单时掏出钱包所有零钱,刚好还差三毛,问能否优惠三毛。收银美女很直接地答应了。付了钱出门时,收银美女还很客气地道了声慢走。(可惜了,我习惯了没礼貌,不知道怎么回)

IMG_20180109_143946

于是满载而归,背着沉重的背包去袁家岭搭地铁回家。

Posted in Uncategorised | Leave a comment

Debian stretch安装nvidia显卡驱动,并配置双显卡切换

机器是戴尔老爷机,Dell inspiron 14r 7420,显卡为Intel + Nvidia。

之前Debian wheezy上双显卡解决方案见我的这篇。建议先阅读该篇。

下面是我解决问题之后的简单总结。实际摸索过程远比这复杂。各人机器环境不同,下面的方案绝非放之四海皆准——仅供参考!

第一步,为默认源添加contrib non-free组件,并更新本地仓库缓存。

第二步,安装nvidia-driver驱动:

sudo apt-get install linux-headers-$(uname -r|sed ‘s,[^-]*-[^-]*-,,’) nvidia-driver

不要执行nvidia-xconfig!

第三步,安装bumblebee-nvidia,支持双显卡切换:

sudo apt-get install bumblebee-nvidia primus

添加当前用户到bumblebee用户组:

sudo adduser $USER bumblebee

第四步,重启

 

如果遇到问题,检查/var/log/Xorg.0.log、/var/log/user.log

参考:

NvidiaGraphicsDrivers – Debian Wiki

Bumblebee – Debian Wiki

双显卡机器安装Debian Wheezy运行Steam游戏

Posted in Uncategorised | Tagged , , | Leave a comment

遭遇MongoDB勒索

最近测试环境表现得不正常,MongoDB的数据展现不出来。
查了日志,发现有这么几条:

2017-02-08T23:28:32.384+0000 I COMMAND [conn773] dropDatabase PLEASE_READ_ME starting
2017-02-08T23:28:32.388+0000 I COMMAND [conn773] dropDatabase PLEASE_READ_ME finished
2017-02-08T23:28:34.581+0000 I COMMAND [conn773] dropDatabase iwesee starting
2017-02-08T23:28:34.601+0000 I COMMAND [conn773] dropDatabase iwesee finished

我的代码中不可能有dropDatabase的操作,也没这么手动操作过。

既然PLEASE_READ_ME,那我去就读。
在数据库中发现了PLEASE_READ_ME库,库下有PLEASE_READ_ME集合,集合中有这么个文档:

{ 
    "_id" : ObjectId("58a06f3a6d3d693e86ae59da"), 
    "info" : "Don't panic. Your DB is in safety and backed up (check logs). To restore send 0.05 BTC and email with your server ip or domain name. Each 48 hours we erase all data.", 
    "amount" : "0.05 BTC", 
    "data_we_have" : {
        "DB_H4CK3D" : [
            "URG3NT_W4RN1NG"
        ], 
        "iwesee" : [
            "ticket.subject.movie.nowplaying", 
            "ticket.subject.movie.later", 
            "ticket.subject.photo", 
            "ticket.subject"
        ]
    }, 
    "Bitcoin Address" : "16bm9fMWdmGCmhXDm3QqMgzwAFwPzLpGaR", 
    "email" : "kraken8888@sigaint.org"
}

日志中还有不少国外的ip连接记录。

于是确认自己遇上了传说中的数据库勒索者了。

0.05BTC我是没有。数据库也不过是测试数据库,删除了还能自动重建。当初为了方便访问,才绑定公网地址并没设密码。于是重建mongo容器,改为绑定内网地址,并趁这个机会,升级到3.4。以后访问,也只好加SSH通道。

Posted in Uncategorized | Tagged | 1 Comment