基于Redis的限流系统的设计

本文讲述基于Redis的限流系统的设计,主要会谈及限流系统中限流策略这个功能的设计;在实现方面,算法使用的是令牌桶算法来,访问Redis使用lua脚本。

1、概念

In computer networks, rate limiting is used to control the rate of traffic sent or received by a network interface controller and is used to prevent DoS attacks

用我的理解翻译一下:限流是对系统的出入流量进行控制,防止大流量出入,导致资源不足,系统不稳定。

限流系统是对资源访问的控制组件,控制主要的两个功能:限流策略熔断策略,对于熔断策略,不同的系统有不同的熔断策略诉求,有的系统希望直接拒绝、有的系统希望排队等待、有的系统希望服务降级、有的系统会定制自己的熔断策略,很难一一列举,所以本文只针对限流策略这个功能做详细的设计。

针对限流策略这个功能,限流系统中有两个基础概念:资源和策略。

  • 资源 :或者叫稀缺资源,被流量控制的对象;比如写接口、外部商户接口、大流量下的读接口
  • 策略 :限流策略由限流算法和可调节的参数两部分组成

熔断策略:超出速率阈值的请求处理策略,是我自己理解的一个叫法,不是业界主流的说法。

2、限流算法

  • 限制瞬时并发数
  • 限制时间窗最大请求数
  • 令牌桶

2.1、限制瞬时并发数

定义:瞬时并发数,系统同时处理的请求/事务数量

优点:这个算法能够实现控制并发数的效果

缺点:使用场景比较单一,一般用来对入流量进行控制

java伪代码实现

AtomicInteger atomic = new AtomicInteger(1)
try {    
    if(atomic.incrementAndGet() > 限流数) {   
        //熔断逻辑
    } else {
        //处理逻辑
    } 
} finally {
    atomic.decrementAndGet();
}

2.2、限制时间窗最大请求数

定义:时间窗最大请求数,指定的时间范围内允许的最大请求数

优点:这个算法能够满足绝大多数的流控需求,通过时间窗最大请求数可以直接换算出最大的QPS(QPS = 请求数/时间窗)

缺点:这种方式可能会出现流量不平滑的情况,时间窗内一小段流量占比特别大

lua代码实现

--- 资源唯一标识
local key = KEYS[1]
--- 时间窗最大并发数
local max_window_concurrency = tonumber(ARGV[1])  
--- 时间窗
local window = tonumber(ARGV[2])   
--- 时间窗内当前并发数
local curr_window_concurrency = tonumber(redis.call('get', key) or 0)  
if current + 1 > limit then
    return false
else
    redis.call("INCRBY", key,1)    
    if window > -1 then
        redis.call("expire", key,window)    
    end
    return true
end

2.3、令牌桶

算法描述

  • 假如用户配置的平均发送速率为r,则每隔1/r秒一个令牌被加入到桶中
  • 假设桶中最多可以存放b个令牌。如果令牌到达时令牌桶已经满了,那么这个令牌会被丢弃
  • 当流量以速率v进入,从桶中以速率v取令牌,拿到令牌的流量通过,拿不到令牌流量不通过,执行熔断逻辑

属性

  • 长期来看,符合流量的速率是受到令牌添加速率的影响,被稳定为:r
  • 因为令牌桶有一定的存储量,可以抵挡一定的流量突发情况
    • M是以字节/秒为单位的最大可能传输速率:M>r
    • T max = b/(M-r)    承受最大传输速率的时间
    • B max = T max * M   承受最大传输速率的时间内传输的流量

优点:流量比较平滑,并且可以抵挡一定的流量突发情况

因为我们限流系统的实现就是基于令牌桶这个算法,具体的代码实现参考下文。

3、工程实现

3.1、技术选型

  • mysql:存储限流策略的参数等元数据
  • redis+lua:令牌桶算法实现

说明:因为我们把redis 定位为:缓存、计算媒介,所以元数据都是存在db中

3.2、架构图

3.3、 数据结构

字段 描述
name 令牌桶的唯一标示
apps 能够使用令牌桶的应用列表
max_permits 令牌桶的最大令牌数
rate 向令牌桶中添加令牌的速率
created_by 创建人
updated_by 更新人

限流系统的实现是基于redis的,本可以和应用无关,但是为了做限流元数据配置的统一管理,按应用维度管理和使用,在数据结构中加入了apps这个字段,出现问题,排查起来也比较方便。

3.4、代码实现

3.4.1、代码实现遇到的问题

参考令牌桶的算法描述,一般思路是在RateLimiter-client放一个重复执行的线程,线程根据配置往令牌桶里添加令牌,这样的实现由如下缺点:

  • 需要为每个令牌桶配置添加一个重复执行的线程
  • 重复的间隔精度不够精确:线程需要每1/r秒向桶里添加一个令牌,当r>1000 时间线程执行的时间间隔根本没办法设置(从后面性能测试的变现来看RateLimiter-client 是可以承担 QPS > 5000 的请求速率)

3.4.2、解决方案

基于上面的缺点,参考了google的guava中RateLimiter中的实现,我们使用了触发式添加令牌的方式。

算法描述

  • 基于上述的令牌桶算法
  • 将添加令牌改成触发式的方式,取令牌的是做添加令牌的动作
  • 在去令牌的时候,通过计算上一次添加令牌和当前的时间差,计算出这段间应该添加的令牌数,然后往桶里添加
    • curr_mill_second = 当前毫秒数
    • last_mill_second = 上一次添加令牌的毫秒数
    • r = 添加令牌的速率
    • reserve_permits = (curr_mill_second-last_mill_second)/1000 * r
  • 添加完令牌之后再执行取令牌逻辑

3.4.3、 lua代码实现

--- 获取令牌
--- 返回码
--- 0 没有令牌桶配置
--- -1 表示取令牌失败,也就是桶里没有令牌
--- 1 表示取令牌成功
--- @param key 令牌(资源)的唯一标识
--- @param permits  请求令牌数量
--- @param curr_mill_second 当前毫秒数
--- @param context 使用令牌的应用标识
local function acquire(key, permits, curr_mill_second, context)
    local rate_limit_info = redis.pcall("HMGET", key, "last_mill_second", "curr_permits", "max_permits", "rate", "apps")    
    local last_mill_second = rate_limit_info[1]    
    local curr_permits = tonumber(rate_limit_info[2])    
    local max_permits = tonumber(rate_limit_info[3])    
    local rate = rate_limit_info[4]    
    local apps = rate_limit_info[5]    
    --- 标识没有配置令牌桶
    if type(apps) == 'boolean' or apps == nil or not contains(apps, context) then
        return 0
    end
    local local_curr_permits = max_permits;    
    --- 令牌桶刚刚创建,上一次获取令牌的毫秒数为空
    --- 根据和上一次向桶里添加令牌的时间和当前时间差,触发式往桶里添加令牌
    --- 并且更新上一次向桶里添加令牌的时间
    --- 如果向桶里添加的令牌数不足一个,则不更新上一次向桶里添加令牌的时间
    if (type(last_mill_second) ~= 'boolean' and last_mill_second ~= false and last_mill_second ~= nil) then
        local reverse_permits = math.floor(((curr_mill_second - last_mill_second) / 1000) * rate)        
        local expect_curr_permits = reverse_permits + curr_permits;
        local_curr_permits = math.min(expect_curr_permits, max_permits);       
         --- 大于0表示不是第一次获取令牌,也没有向桶里添加令牌
        if (reverse_permits > 0) then
            redis.pcall("HSET", key, "last_mill_second", curr_mill_second)       
      end
    else
        redis.pcall("HSET", key, "last_mill_second", curr_mill_second)   
    end
    local result = -1
    if (local_curr_permits - permits >= 0) then
        result = 1
        redis.pcall("HSET", key, "curr_permits", local_curr_permits - permits)    
    else
        redis.pcall("HSET", key, "curr_permits", local_curr_permits)    
    end
    return result
end

关于限流系统的所有实现细节,我都已经放到github上,gitbub地址:https://github.com/wukq/rate-limiter,有兴趣的同学可以前往查看,由于笔者经验与知识有限,代码中如有错误或偏颇,欢迎探讨和指正。

3.4.4、管理界面

前面的设计中,限流的配置是和应用关联的,为了更够更好的管理配置,需要一个统一的管理页面去对配置进行管控:

  • 按应用对限流配置进行管理
  • 不同的人分配不同的权限;相关人员有查看配置的权限,负责人有修改和删除配置的权限

3.5、性能测试

配置:aws-elasticcache-redis 2核4g

因为Ratelimiter-client的功能比较简单,基本上是redis的性能打个折扣。

  • 单线程取令牌:Ratelimiter-client的 QPS = 250/s
  • 10个线程取令牌:Ratelimiter-client的 QPS = 2000/s
  • 100个线程取令牌:Ratelimiter-client的 QPS = 5000/s

4、总结

限流系统从设计到实现都比较简单,但是确实很实用,用四个字来形容就是:短小强悍,其中比较重要的是结合公司的权限体系和系统结构,设计出符合自己公司规范的限流系统。

不足

  • redis 我们用的是单点redis,只做了主从,没有使用redis高可用集群(可能使用redis高可用集群,会带来新的问题)
  • 限流系统目前只做了应用层面的实现,没有做接口网关上的实现
  • 熔断策略需要自己定制,如果实现的好一点,可以给一些常用的熔断策略模板

参考书籍:

1.《Redis 设计与实现》
2.《Lua编程指南》

参考文章:

1. redis官网

2. lua编码规范

3. 聊聊高并发系统之限流特技

4. guava Ratelimiter 实现

5. Token_bucket wiki 词条

Web网页爬虫对抗指南 Part.1

从本质上来讲,阻止Web 爬虫就意味着你需要让脚本和机器难以从你的网站上获取它们所需的数据,但不能让那些真正的用户和搜索引擎爬取数据变得困难。

然而不幸的是,要做到这一点很难,你需要在防止Web 爬虫和降级真实用户和搜索引擎的可访问性之间进行权衡。

为了阻止Web 爬虫(也称为Webscraping,Screenscraping,Web数据挖掘,Web收集或Web数据提取),你需要了解这些爬虫的工作原理,以及它们为什么能够运行良好的原因,本文的内容将会告诉你阻止爬虫爬取数据的答案。

一般来说,这些爬虫程序是为了从你的网站提取特定的信息,如文章,搜索结果,产品详细信息,或你的案例中爬取艺术家和专辑等信息。通常,人们使用爬虫爬取特定的数据的目的是将这些数据在自己的网站上进行重用(并从你的内容中赚自己的钱),或为你的网站(如移动应用程序)构建一种“假冒的”替代方案,甚至用于私人研究或分析目的。

基本上,网络中存在着各种类型的爬虫,每一种爬虫的工作方式都不尽相同:

· 蜘蛛(Spider),例如谷歌的机器人或网站复制工具,比如HTTrack,这中爬虫会访问你的网站,并递归地跟随到其他页面的链接,以获取数据。这些爬虫有时用于定向的爬取来获取特定的数据,通常与HTML解析器结合,然后从每个页面中提取所需要的数据。

· Shell脚本:有时候,常用的Unix工具也用于爬取数据,例如:使用shell脚本执行Wget或Curl下载Web页面,通常再结合Grep(使用正则表达式)提取所需的数据。这些是最简单的爬虫,也是最脆弱的一种爬虫(不要尝试使用正则表达式解析HTML!)。

· HTML爬虫和解析器,例如基于JsoupScrapy等等的爬虫技术。这类爬虫有点类似于基于shell脚本的正则表达式的爬虫,都是通过基于在HTML中进行正则匹配提取你的页面中的数据,并忽略其他的内容。

所以,如果你的网站具有搜索功能,那么这样的爬虫可能会提交一个搜索的HTTP请求,然后从结果页的HTML中获取所有结果的链接及其标题,有时候会发起请求数百次和数百种不同的搜索行为,以便专门获取搜索结果链接和网页标题。这些爬虫是最常见的一种。

·  屏幕爬虫,这类爬虫是基于例如 SeleniumPhantomJS编写的。实际上这种爬虫是在真正的浏览器中打开了你的网站,并运行JavaScript,AJAX等等,然后再从网页中获取所需的文本,一般的做法如下:

o   在页面加载和JavaScript运行之后,从浏览器中获取HTML,然后使用HTML解析器来提取所需的数据或文本。这些是最常见的爬取数据的手法,这些方法在HTML解析器和其他的爬虫方法不能用的情况下也有效。

o   对渲染页面进行屏幕截图,然后使用OCR技术从屏幕截图中提取所需的文本。这中方法比较罕见,只有在爬取数据没有办法的情况下才会使用这种专用的爬虫。

基于浏览器的屏幕爬虫更难处理,因为它们运行脚本,渲染HTML,并且可以像一个真正的人在浏览你的网站一样。

· 在线的Web爬虫服务,如ScrapingHubKimono。事实上,有些人的工作就是弄清楚如何抓取你的网站的数据,并提出内容给别人使用。这类爬虫技术,有时候会使用庞大的代理网络和不断变化的IP地址来突破限制和阻止,所以它们是问题比较严重的一类爬虫。

专业的爬虫服务是最难以阻止的,这不需要感到奇怪,但是如果你弄清楚Web 爬虫是如何爬取你的网站的,那么这些爬虫(以及为这类爬虫服务付款方式的人)可能不会因为你的网站的数据而遭到困扰了。

· 将你的网站使用框架嵌入其他网站的网页中,并将你的网站嵌入到移动应用中。

虽然在技术上来说这种手法不算是爬虫,但这的确也算是一个问题,因为移动应用程序(Android和iOS)可以嵌入你的网站,甚至注入自定义的CSS和JavaScript,从而完全改变你的网站的外观,并且只显示所需的信息,如文章内容本身或搜索结果列表,以及隐藏一些东西,如页眉,页脚或广告。

· 人工复制和粘贴:网站访客会复制和粘贴你的网站的内容,以便在其他地方使用。不幸的是,要阻止这种方法你基本上无能为力。

这些不同类型的爬虫之间存在很多重叠的地方,即使使用不同的技术和方法来获取你的内容,许多爬虫也将表现出相似的行为。

以上这些提示信息大部分是我自己的想法,包括我在编写爬虫时遇到的各种困难,以及来自互联网上的一些信息和想法。

如何阻止爬虫

一些检测和阻止爬虫的常见方法:

监控你的日志和流量模式; 如果发现异常活动,则限制访问:

定期检查你的日志,如果发现了一些自动访问(爬虫)的异常活动(例如来自同一个IP地址的许多类似的请求操作),那么你可以阻止或限制这些IP地址的访问。

具体来说,限制措施如下:

· 访问速率限制:

只允许用户(和爬虫)在一定时间内执行有限数量的操作 – 例如,只允许任何特定的IP地址或用户每秒进行几次搜索。这样就会减慢爬虫的使用,使爬取功能逐渐无效。如果操作的完成速度比实际用户要快得多或更快,你也可以显示一个验证码来阻止爬虫。

· 检测异常活动:

如果你发现了异常活动,例如来自某个特定的IP地址的许多类似的请求,或者是某个用户浏览了过多的页面或执行的搜索次数不同寻常,那么也可以阻止这类IP或用户的访问,或者是在后续的请求中显示验证码。

· 不要只是通过IP地址监控和限制访问速率 —— 也可以使用其他指标:

如果你阻止或限制访问速率,请不要仅在每个IP地址的基础上进行; 你可以使用其他指标和方法来识别特定的用户或爬虫。下面包括了一些可以帮助你识别特定用户/爬虫的指标:

o   用户填写表单的速度以及他们点击的按钮的位置;

o   你可以使用JavaScript收集客户端的大量信息,例如屏幕尺寸/分辨率,时区,已安装的字体等; 你可以使用这些信息来识别用户。

o   Http协议头及其顺序,尤其是User-Agent。

例如,如果你发现从单个IP地址上发出了许多请求,则所有使用相同的User agent,屏幕尺寸(用JavaScript确定)的用户(此时应该是爬虫)始终以相同的方式和相同的时间间隔定期点击按钮,那么它可能是一个屏幕爬虫; 你可以暂时阻止类似的请求(例如,阻止来自特定的IP地址中相同的user agent和屏幕大小的所有请求),这样你就不会对该IP地址上的真实用户造成影响,例如:在共享互联网连接的情况下。

你还可以进一步的阻止爬虫,因为你可以识别类似的请求,如果这些请求来自不同的IP地址,那么意味着这可能是一个分布式Web 爬虫(使用僵尸网络或代理网络的爬虫)。如果你发现了还有很多相同的其他请求,但是它们来自不同的IP地址,那么你也可以阻止IP地址的访问。再次提醒一下,不要在无意中阻止了真实的用户。

这可以对运行JavaScript的屏幕截图类的爬虫很有效,因为你可以从中获取大量的信息。

Security Stack Exchange上面与此有关的相关问题:

· 如何识别出口IP地址相同的用户?

· 为什么人们在IP地址经常变化时使用IP地址黑名单?

· 请使用验证码替代临时阻止访问的方案:

实现访问速率限制的简单方法是在一段时间内临时阻止访问,但是使用验证码可能会更好一些,详细内容请参阅“验证码”部分。

要求注册和登录

如果你的网站可以做到需要创建帐户才能查看你的网站的内容的话,那么这对爬虫来说可能是一个很好的威慑,但对真实用户来说也可能是一个很好的威慑。

· 如果你要求客户端创建帐户和登录,那么你可以准确的跟踪用户和爬虫的操作。这样的话,你就可以轻松的检测某些帐户是何时被用于爬虫的,然后禁止掉这些用户。访问速率限制或检测滥用(例如在短时间内的大量搜索请求)也会变得更加容易,因为你可以识别特定的爬虫而不仅仅是识别出IP地址。

为了避免自动化创建许多帐户的脚本,你应该做以下这些事情:

· 注册时需要一个电子邮件地址,并且用户必须打开通过你所发送的链接来验证该电子邮件地址才能激活该帐户。每个电子邮件地址只允许一个帐户。

· 需要在注册/帐户创建期间显示验证码,以防止自动化脚本创建帐户。

要求创建帐户才能查看内容的做法会导致用户和搜索引擎不能正常访问; 如果你需要创建帐户才能查看文章,那么用户将会去其他网站,而不是去注册一个账户然后再查看文章。

阻止来自云托管主机和爬虫服务的IP地址的访问

有时,爬虫会从网络托管服务(如Amazon Web Services或Google App Engine)或VPSes运行。限制(或显示验证码)来源于此类云主机服务使用的IP地址请求访问你的网站。你还可以阻止从爬虫服务使用的IP地址的访问。

同样,你还可以限制代理或VPN提供商使用的IP地址的访问,因为爬虫可能会使用此类代理服务器来避免单个IP发出许多请求。

请注意,通过阻止代理服务器和VPN的访问,你将对真实用户产生负面的影响。

如果你要采取阻止访问的做法,请将你的错误消息进行调整

如果你采取了阻止/限制访问的手段,那么你应该确保不要告诉爬虫导致阻塞的实际原因,从而给爬虫制作者如何修复爬虫的线索。所以用文本显示错误页面可能是一个坏主意:

· 你的IP地址请求过于频繁,请稍后再试。

· 错误,User Agent不存在,非法访问!

相反,显示一个友好的错误消息,不要告诉爬虫是什么原因造成不能正常访问的。

· 抱歉,网站出了一些问题。如果问题仍然存在,你可以联系helpdesk@example.com。

这也是真正对用户友好的一种显示内容。你还应该考虑在后续的请求中不要直接显示验证码,以防真实用户看到这个错误消息,导致你阻止了合法用户的访问行为。

如果你的网站被爬虫访问,请使用Captchas。

Captchas(“完全自动化测试以分辨电脑和人类”)对于阻止爬虫非常有效。不幸的是,这种方式对正常用户的请求有干扰。

因此,当你怀疑你的网站可能被爬了,那么这种方式是非常有用的,因为这种方法只是阻止了Web 爬虫,而不会阻止真实的正常用户的访问。

使用Captchas时需要注意下面几个事情:

·  不要自己实现验证码,应该使用像Google的reCaptcha这样的东西:这比自己实现一个验证码要容易得多,它比一些模糊和扭曲的文本解决方案对用户来说更友好,而且比起自己实现验证码,它能更好的解决问题

·  不要在HTML标记中包含验证码的解决方案:实际上我已经看到一个网站在页面中嵌入了验证码的解决方案(虽然隐藏的很好)。但是不要这样做。还是推荐使用像reCaptcha这样的服务,如果你正确使用它,就不会出现问题。

· Captchas是可以批量验证的:在网上有一些低报酬的人工打码服务可以批量解决验证码的验证服务。所以,我在这里还是推荐使用reCaptcha,因为它具有很好的自我保护作用。这种服务不太可能被用于人工打码,除非你的数据真的很有价值。

将你的文本内容转成图片

你可以在服务器端将文本转换成图像然后在客户端显示,这种方法可以阻碍简单的爬虫提取文本。

然而,这对于屏幕阅读器,搜索引擎,性能以及其他的一些事情都是不利的。在某些方面(比如无障碍操作,例如美国残疾人法案)也可能是非法的,并且也很容易被一些OCR技术绕过,所以最好还是不要这样做。

你可以做类似于CSS精灵的东西,但是也同样会遇到上面的问题。

不要公开你的完整数据集:

如果可行的话,请不要为脚本或爬虫机器人提供所有数据集的方法。例如:你有一个新闻网站,有很多单独的文章。你可以通过现场搜索来搜索这些文章,并且如果你没有列出任何网站上的所有文章及其URL,那么这些文章将只能通过使用搜索来访问特征。这意味着一个想要从你的网站上获得所有文章的脚本将不得不搜索可能出现在你的文章中的所有可能的短语,才能找到文章的全部内容,这对于爬虫来说是很耗时的,而且效率低的可怕,所以爬虫一般都会放弃爬取数据。

如果是以下这些情况,那么这种方法将变得无效:

· 爬虫机器人或脚本并不想或需要完整的数据集。

· 你的文章的URL是一个看起来像 example.com/article.php?articleId=12345这种(和类似的东西)的格式,这将允许爬虫可以简单地迭代所有articleId的文章,并请求所有的文章内容。

· 还有其他方式可以最终找到所有的文章,比如通过编写一个脚本来跟踪其他文章的内容中的链接。

· 搜索“&”或“”的东西可以显示几乎所有的东西,这是一些需要注意的事情。(你只能返回前10或20个结果来避免这种情况)。

· 你需要搜索引擎来查找你的内容。

本文翻译自:https://github.com/JonasCz/How-To-Prevent-Scraping/blob/master/README.md ,如若转载,请注明原文地址: http://www.4hou.com/technology/8482.html?hmsr=toutiao.io&utm_medium=toutiao.io&utm_source=toutiao.io

开源项目挣钱实用手册

我列出了我从各种渠道所知道的开源项目带来收入的人们的故事,每种出资类型都有一系列的真实案例,粗略的按照出资量从小到大排列。(我已尽可能的将链接指向具体故事而非主页面)

本文中的出资类型并非互斥的,例如一个项目既可以由基金会也可以通过众筹来筹集资金,而一个人既可以靠咨询挣钱也可以获得捐赠。本文的主要目的是提供一个详尽的挣钱方式列表,而你只需要从中选出适合你的。

本中文版是原版的翻译版本。

原项目名称Lemonade Stand是指销售柠檬汁的小摊,而在美国,这种小摊通常是由小朋友运作的。


目录

  1. 捐赠按钮
  2. 悬赏
  3. 众筹(一次性)
  4. 众筹(持续性)
  5. 卖书及周边
  6. 广告
  7. 受雇于公司并继续你的项目
  8. 在职时启动项目
  9. 补贴
  10. 咨询服务
  11. SaaS
  12. 双重协议
  13. 开放核心
  14. 基金会
  15. 风险投资

附录: 贡献 // 协议

“个人努力” 用来标记其资金是由个人而非项目主导筹集获得的

捐赠按钮

在你的网站页面里放上捐赠按钮。Stripe和PayPal都可以很方便的提供这项服务。

优点

  • 限制条件少
  • 工作量小:放好后就可以不管了

缺点

  • 除非你努力筹款,通常都不会有太多钱
  • 需要一个法人实体来接受捐赠(SFCOpenCollective可在这方面提供帮助),因此较难管理个人的国际性捐赠
  • 在多人项目中很难明确如何分配这笔捐赠

案例学习

悬赏

项目或公司可能时不时的张贴开源项目的悬赏工作(例如“修漏洞赚$100”),下面列出了一些网站,他们收集和发布了这类悬赏工作。

优点

  • 参与到开源社区
  • 确定的工作有确定的回报
  • 在修复安全漏洞方面特别流行

缺点

  • 会在项目中产生不合理的激励机制(低质量的PR也会影响项目的专注性)
  • 通常没多少钱 (~<$500)
  • 无法提供持久的收入

案例学习

众筹(一次性)

如果你想实现一个特别的想法(区别于长期项目),一次性的众筹活动可以帮助你筹集到你需要的资金,许多个人和公司都可能会为你的想法捐款。

优点

  • 限制条件少
  • 对个人来说也可以很容易合法的进行,例如通过Kickstarter

缺点

  • 一大堆的市场工作需要做
  • 通常都需要为捐赠者提供回报甚至是特权
  • 通常钱也不是那么多(一次大约$50K)
  • 公司并不常愿意向众筹捐款

案例学习

众筹(持续性)

如果你想要为持续性的项目筹集资金,可以设立一个持续性的众筹,捐赠者承诺按月或按年提供资金直到捐赠者退出。对于那些经常使用你的项目的个人或公司可能会愿意为你的项目提供资金。

优点

缺点

  • 很难获得承诺的持续性捐赠(这通常需要你或项目已经有一定的名声)
  • 很难解释持续性的捐赠能获得什么样明确的回报或特权
  • 通常钱也不是那么多(一个月$1-4K)

案例学习

卖书及周边

如果你是某个领域的专家,你可以写书卖书,可以找个出版社(像O’Reilly)或自己出版(译者注:在中国不行)。除了卖书之外,有些项目也卖短袖外套等。

优点

  • 筹集到的资金并不和项目本身管理关联,所以项目本身可以保持创作的自由
  • 销售的商品本身也可以当做为项目做的宣传
  • 在初次销售后可以成为持续性的资金来源

缺点

  • 通常钱也不是那么多
  • 会影响用在项目上的精力
  • 卖周边需要准备预付资金

案例学习

广告

如果你的项目已经有了一定的受众,你可以帮助广告商向你的受众推销。通常你的项目都会有明确的受众,这是你的优势也是广告商所喜欢的。(比如你有一个Python项目,那么基本上可以假定你的受众一定是技术上熟悉Python的)

优势

  • 这是成熟明确而且大众也能接受的商业模式

缺点

  • 需要足够多的受众才能请来广告商
  • 需要通过透明化来让受众相信你(比如让其信任你不会追踪他们)
  • 需要许多精力来寻找和管理广告商

案例学习

受雇于公司并继续你的项目

公司有时候会雇佣一些个人来做开源项目,你可以寻找一个正在使用你的开源项目的公司。当然具体在公司里可能公司工作和开源项目工作时间会是五五分。除此之外,也可以找一个愿意尝试使用你的新项目的公司。如果你有展示项目经验,这将会非常有用。

优点

  • 可以利用公司的资源
  • 可以很好的和公司的需求保持一致
  • 稳定的收入

缺点

  • 获得这样的机会需要极好的运气,现目前没有明确可重复的方式获得这样的机会
  • 项目通常需要非常出名并且被使用
  • 对于没有为公司的利润工作,这使得个人很容易被公司优先舍弃
  • 公司可能会过分影响项目的发展
  • 可能会因平衡不好两边而影响项目

案例学习

在职时启动项目

许多开源项目最初都是员工的编外项目(Side Project),即便其最终可能会成长为一家公司,但以编外项目的形式在公司里进行孵化应该是不错的选择。

如果你想走这条路,请确定你理解了公司在开源项目上的政策。有些公司鼓励员工在工作时间从事开源项目开发,而有些则将你的任何工作视作公司项目。不要假定任何前提,最好在开始前问问你公司里的相关人员。

优点

  • 可以不用担心收入的情况下尝试新想法
  • 可以和公司的需求很好的保持一致
  • 适合尝试新想法

缺点

  • 需要用业余时间开发,或者获准在工作时间开发
  • 有被公司过分影响的风险
  • 持续下去可能会出现极度复杂的管理情况

案例学习

补贴

补贴是不需要偿还的极其有效的大笔捐赠,提供补贴的组织通常能够通过给予补贴而从其他方面获得利益,例如接近你,展示其影响力,获得你的工作汇报或税率优惠。

补贴可能来自很多地方,包括公司、软件基金会、慈善基金会以及政府,其技术及法律方面会因其来源的不同有很大的差异。比如一家公司可以通过开咨询费发票给你补贴,而慈善基金会则只能给非盈利组织或个人,通常你得找到一个非盈利组织来帮助你。如果你对补贴不熟悉,了解它的最好方式就是和曾经获得过的人去了解。下面列出了一些成功的案例

优点

  • 限制条件少
  • 有保证的资金可以确保你能在一段时间里专注在你的项目上
  • 让你的项目有时间喘口气和做些试验

缺点

  • 软件方面没太多提供补贴的组织
  • 补贴是有限的,始终需要在用完补贴前找到一个持续方法

案例学习

咨询服务

咨询是一种为开源项目提供资金的灵活方式。你可以更加自由的规划你的时间,比如一周30小时做咨询业务,10小时做开源项目。咨询师通常收费相对较贵,因为工作不稳定且没有公司福利。如果你打算做这类公司,你应该想要创建一家有限责任公司。

优点

  • 这是成熟明确而且大众也能接受的商业模式

缺点

  • 咨询工作需要人力,而且不宜规模化(除了极少数)
  • 商业本身的需求会分散开源项目上的注意力
  • 可能会和软件的简易要求有差异
  • 项目需要足够流行,才会让人愿意为相关服务付钱

案例学习

SaaS

SaaS 即 软件即服务(Software as a Service)。在这个模型下,代码本身是开源的,但是你可以提供增值服务使得用户更容易使用。一个典型的增值服务就是托管付费。

优点

  • 可以围绕这个开源项目建立一个社区,然后通过托管服务赚钱
  • 让开源项目可以专注在用户层面,当需求增加后再帮助企业采用这个项目
  • 可以根据用户数进行规模化改造

缺点

  • 对增值服务使用者来说,通常意味着托管服务必须比招聘一个人来维护项目更便宜
  • 增值服务有可能会使得免费用户不太高兴

案例学习

双重协议

有时候项目可以在完全相同的代码里提供两种不同的授权协议:一个是商业友好的,而另外一个则不(例如GPL)。后者对于个人来说可以免费使用,而公司需要通过购买商业协议来获得合法的商业授权。

优点

  • 这是成熟明确而且大众也能接受的商业模式
  • 如果成功的话,也可以做到规模化

缺点

  • 会和让软件自由获得的理想有冲突
  • 项目需要足够大以满足客户的需求

案例学习

开放核心

开放核心模型中,项目的一些方面是免费的,但一些功能则是私有的,且只对付费用户开放,通常要求这个项目有企业的需求。

优点

  • 这是成熟明确而且大众也能接受的商业模式
  • 如果成功的话,也可以做到规模化

缺点

  • 需要设计付费项目,且该项目应该是独有的
  • 会和让软件自由获得的理想有冲突
  • 独有功能有可能会使得免费用户不太高兴

案例学习

基金会

基金会是可以接收和支出的合法法人实体。鉴于其目的并非获得利润,因此可以更好保持中立地管理项目。在美国,基金会可以是501c3(非盈利)或501c6(贸易联盟),许多软件基金会都是贸易联盟,因为非盈利基金会要求展示出慈善的目的,这在软件开发中比较困难。

优点

  • 中立,基金会可以帮助保护代码和管理社区
  • 可以在许多捐赠者中散布影响力
  • 使得项目合法,公司会更愿意向基金会付款/捐赠而非个人

缺点

  • 只适合大项目
  • 由于IRS的限制,项目能做什么会有所限制
  • 需要大量社区的努力和各种技能,而且之后仍旧需要努力获得筹款

案例学习

风险投资

风险投资是高增长业务的一种筹资形式,不像银行贷款或者任意一种债务财务形式,风险投资者通过提供资金来占有你业务的一定股份。这种交易不像贷款,如果你的业务挂了你并不需要偿还出资方。当然,如果你成功了,你也需要成倍的返还给你的投资者。

风险投资是高风险高回报的:风投者相比银行对风险更加宽容,当然他们也期待你成功后的巨额回报。如果你打算获得风险投资,你应该建立股份有限公司。如果你对风险投资过程不熟悉,开始的最好方式就是询问成功获得风险投资的人。

优点

  • 制度上的支持对成长中的业务有益
  • 大量的风投资金蓄势待发

缺点

  • 风险投资在投资之初便做好了获得成倍回报后退出的打算,历史证明,由于开源项目的结构特点,风险投资的成功很难。
  • 风险投资者可能会因为优先级改变其动机

(译者注:在中国有一类风险投资者故意诱导涉世不深的创业者签订不平等协议,尤其是在创业者热情最浓而又最困难的时候,使其在业务失败时也能全身而退,相应的,创业者会输得很惨,值得小心对待)

案例学习

贡献

我写这个手册主要是为了将我头脑中的知识都整理出来,不过我并没有打算做主要的贡献或改变。优点和缺点大多是从我的观点出发的主观想法。

如果有什么错误(尤其是案例学习),非常欢迎大家的修改。同时,如果发现有什么分类漏掉了,我也非常欢迎大家的修改。

关于中文版的贡献,如果有信息上的错误或遗失,请到原文中提出,如果有中文翻译上的错误,请在这里直接提出。如果你觉得有信息上的错误或遗失,但又各种原因无法用英文表述,担心中文表述不会被采纳,这种情况请还是在原文中提出,我或者其他同时会英文和中文的小伙伴会帮助解决问题的。

本翻译项目使用翻译辅助工具辅助同步原文修订部分。

协议

原文协议为Creative Commons CC0 1.0 License,即你可以自由的使用,商业或非商业,不过如果你用了,很开心能从你那里听到点什么,在这里找到我@nayafia,当然这并不是要求必须做的。

中文版协议为Creative Commons Attribution 4.0 International License,和原文协议差不多了,基本上可以说怎么用都可以,唯一不要修改原作者名字或以原作者名字声明演绎作品就可以了。

Python 语法速览与实战清单

本文是对于 现代 Python 开发:语法基础与工程实践的总结,更多 Python 相关资料参考 Python 学习与实践资料索引;本文参考了 Python Crash Course – Cheat Sheetspysheeet等。本文仅包含笔者在日常工作中经常使用的,并且认为较为关键的知识点与语法,如果想要进一步学习 Python 相关内容或者对于机器学习与数据挖掘方向感兴趣,可以参考程序猿的数据科学与机器学习实战手册

基础语法

Python 是一门高阶、动态类型的多范式编程语言;定义 Python 文件的时候我们往往会先声明文件编码方式:

# 指定脚本调用方式
#!/usr/bin/env python
# 配置 utf-8 编码
# -*- coding: utf-8 -*-

# 配置其他编码
# -*- coding: <encoding-name> -*-

# Vim 中还可以使用如下方式
# vim:fileencoding=<encoding-name>

人生苦短,请用 Python,大量功能强大的语法糖的同时让很多时候 Python 代码看上去有点像伪代码。譬如我们用 Python 实现的简易的快排相较于 Java 会显得很短小精悍:

def quicksort(arr):
    if len(arr) <= 1:
        return arr
    pivot = arr[len(arr) / 2]
    left = [x for x in arr if x < pivot]
    middle = [x for x in arr if x == pivot]
    right = [x for x in arr if x > pivot]
    return quicksort(left) + middle + quicksort(right)
    
print quicksort([3,6,8,10,1,2,1])
# Prints "[1, 1, 2, 3, 6, 8, 10]"

控制台交互

可以根据 __name__ 关键字来判断是否是直接使用 python 命令执行某个脚本,还是外部引用;Google 开源的 fire 也是不错的快速将某个类封装为命令行工具的框架:

import fire

class Calculator(object):
  """A simple calculator class."""

  def double(self, number):
    return 2 * number

if __name__ == '__main__':
  fire.Fire(Calculator)

# python calculator.py double 10  # 20
# python calculator.py double --number=15  # 30

Python 2 中 print 是表达式,而 Python 3 中 print 是函数;如果希望在 Python 2 中将 print 以函数方式使用,则需要自定义引入:

from __future__ import print_function

我们也可以使用 pprint 来美化控制台输出内容:

import pprint

stuff = ['spam', 'eggs', 'lumberjack', 'knights', 'ni']
pprint.pprint(stuff)

# 自定义参数
pp = pprint.PrettyPrinter(depth=6)
tup = ('spam', ('eggs', ('lumberjack', ('knights', ('ni', ('dead',('parrot', ('fresh fruit',))))))))
pp.pprint(tup)

模块

Python 中的模块(Module)即是 Python 源码文件,其可以导出类、函数与全局变量;当我们从某个模块导入变量时,函数名往往就是命名空间(Namespace)。而 Python 中的包(Package)则是模块的文件夹,往往由 __init__.py 指明某个文件夹为包:

# 文件目录
someDir/
    main.py
    siblingModule.py

# siblingModule.py

def siblingModuleFun():
    print('Hello from siblingModuleFun')
    
def siblingModuleFunTwo():
    print('Hello from siblingModuleFunTwo')

import siblingModule
import siblingModule as sibMod

sibMod.siblingModuleFun()

from siblingModule import siblingModuleFun
siblingModuleFun()

try:
    # Import 'someModuleA' that is only available in Windows
    import someModuleA
except ImportError:
    try:
        # Import 'someModuleB' that is only available in Linux
        import someModuleB
    except ImportError:

Package 可以为某个目录下所有的文件设置统一入口:

someDir/
    main.py
    subModules/
        __init__.py
        subA.py
        subSubModules/
            __init__.py
            subSubA.py

# subA.py

def subAFun():
    print('Hello from subAFun')
    
def subAFunTwo():
    print('Hello from subAFunTwo')

# subSubA.py

def subSubAFun():
    print('Hello from subSubAFun')
    
def subSubAFunTwo():
    print('Hello from subSubAFunTwo')

# __init__.py from subDir

# Adds 'subAFun()' and 'subAFunTwo()' to the 'subDir' namespace 
from .subA import *

# The following two import statement do the same thing, they add 'subSubAFun()' and 'subSubAFunTwo()' to the 'subDir' namespace. The first one assumes '__init__.py' is empty in 'subSubDir', and the second one, assumes '__init__.py' in 'subSubDir' contains 'from .subSubA import *'.

# Assumes '__init__.py' is empty in 'subSubDir'
# Adds 'subSubAFun()' and 'subSubAFunTwo()' to the 'subDir' namespace
from .subSubDir.subSubA import *

# Assumes '__init__.py' in 'subSubDir' has 'from .subSubA import *'
# Adds 'subSubAFun()' and 'subSubAFunTwo()' to the 'subDir' namespace
from .subSubDir import *
# __init__.py from subSubDir

# Adds 'subSubAFun()' and 'subSubAFunTwo()' to the 'subSubDir' namespace
from .subSubA import *

# main.py

import subDir

subDir.subAFun() # Hello from subAFun
subDir.subAFunTwo() # Hello from subAFunTwo
subDir.subSubAFun() # Hello from subSubAFun
subDir.subSubAFunTwo() # Hello from subSubAFunTwo

表达式与控制流

条件选择

Python 中使用 if、elif、else 来进行基础的条件选择操作:

if x < 0:
     x = 0
     print('Negative changed to zero')
 elif x == 0:
     print('Zero')
 else:
     print('More')

Python 同样支持 ternary conditional operator:

a if condition else b

也可以使用 Tuple 来实现类似的效果:

# test 需要返回 True 或者 False
(falseValue, trueValue)[test]

# 更安全的做法是进行强制判断
(falseValue, trueValue)[test == True]

# 或者使用 bool 类型转换函数
(falseValue, trueValue)[bool(<expression>)]

循环遍历

for-in 可以用来遍历数组与字典:

words = ['cat', 'window', 'defenestrate']

for w in words:
    print(w, len(w))

# 使用数组访问操作符,能够迅速地生成数组的副本
for w in words[:]:
    if len(w) > 6:
        words.insert(0, w)

# words -> ['defenestrate', 'cat', 'window', 'defenestrate']

如果我们希望使用数字序列进行遍历,可以使用 Python 内置的 range 函数:

a = ['Mary', 'had', 'a', 'little', 'lamb']

for i in range(len(a)):
    print(i, a[i])

基本数据类型

可以使用内建函数进行强制类型转换(Casting):

int(str)
float(str)
str(int)
str(float)

Number: 数值类型

x = 3
print type(x) # Prints "<type 'int'>"
print x       # Prints "3"
print x + 1   # Addition; prints "4"
print x - 1   # Subtraction; prints "2"
print x * 2   # Multiplication; prints "6"
print x ** 2  # Exponentiation; prints "9"
x += 1
print x  # Prints "4"
x *= 2
print x  # Prints "8"
y = 2.5
print type(y) # Prints "<type 'float'>"
print y, y + 1, y * 2, y ** 2 # Prints "2.5 3.5 5.0 6.25"

布尔类型

Python 提供了常见的逻辑操作符,不过需要注意的是 Python 中并没有使用 &&、|| 等,而是直接使用了英文单词。

t = True
f = False
print type(t) # Prints "<type 'bool'>"
print t and f # Logical AND; prints "False"
print t or f  # Logical OR; prints "True"
print not t   # Logical NOT; prints "False"
print t != f  # Logical XOR; prints "True" 

String: 字符串

Python 2 中支持 Ascii 码的 str() 类型,独立的 unicode() 类型,没有 byte 类型;而 Python 3 中默认的字符串为 utf-8 类型,并且包含了 byte 与 bytearray 两个字节类型:

type("Guido") # string type is str in python2
# <type 'str'>

# 使用 __future__ 中提供的模块来降级使用 Unicode
from __future__ import unicode_literals
type("Guido") # string type become unicode
# <type 'unicode'>

Python 字符串支持分片、模板字符串等常见操作:

var1 = 'Hello World!'
var2 = "Python Programming"

print "var1[0]: ", var1[0]
print "var2[1:5]: ", var2[1:5]
# var1[0]:  H
# var2[1:5]:  ytho

print "My name is %s and weight is %d kg!" % ('Zara', 21)
# My name is Zara and weight is 21 kg!
str[0:4]
len(str)

string.replace("-", " ")
",".join(list)
"hi {0}".format('j')
str.find(",")
str.index(",")   # same, but raises IndexError
str.count(",")
str.split(",")

str.lower()
str.upper()
str.title()

str.lstrip()
str.rstrip()
str.strip()

str.islower()
# 移除所有的特殊字符
re.sub('[^A-Za-z0-9]+', '', mystring) 

如果需要判断是否包含某个子字符串,或者搜索某个字符串的下标:

# in 操作符可以判断字符串
if "blah" not in somestring: 
    continue

# find 可以搜索下标
s = "This be a string"
if s.find("is") == -1:
    print "No 'is' here!"
else:
    print "Found 'is' in the string."

Regex: 正则表达式

import re

# 判断是否匹配
re.match(r'^[aeiou]', str)

# 以第二个参数指定的字符替换原字符串中内容
re.sub(r'^[aeiou]', '?', str)
re.sub(r'(xyz)', r'\1', str)

# 编译生成独立的正则表达式对象
expr = re.compile(r'^...$')
expr.match(...)
expr.sub(...)

下面列举了常见的表达式使用场景:

# 检测是否为 HTML 标签
re.search('<[^/>][^>]*>', '<a href="#label">')

# 常见的用户名密码
re.match('^[a-zA-Z0-9-_]{3,16}$', 'Foo') is not None
re.match('^\w|[-_]{3,16}$', 'Foo') is not None

# Email
re.match('^([a-z0-9_\.-]+)@([\da-z\.-]+)\.([a-z\.]{2,6})$', 'hello.world@example.com')

# Url
exp = re.compile(r'''^(https?:\/\/)? # match http or https
                ([\da-z\.-]+)            # match domain
                \.([a-z\.]{2,6})         # match domain
                ([\/\w \.-]*)\/?$        # match api or file
                ''', re.X)
exp.match('www.google.com')

# IP 地址
exp = re.compile(r'''^(?:(?:25[0-5]
                     |2[0-4][0-9]
                     |[1]?[0-9][0-9]?)\.){3}
                     (?:25[0-5]
                     |2[0-4][0-9]
                     |[1]?[0-9][0-9]?)$''', re.X)
exp.match('192.168.1.1')

集合类型

List: 列表

Operation: 创建增删

list 是基础的序列类型:

l = []
l = list()

# 使用字符串的 split 方法,可以将字符串转化为列表
str.split(".")

# 如果需要将数组拼装为字符串,则可以使用 join 
list1 = ['1', '2', '3']
str1 = ''.join(list1)

# 如果是数值数组,则需要先进行转换
list1 = [1, 2, 3]
str1 = ''.join(str(e) for e in list1)

可以使用 append 与 extend 向数组中插入元素或者进行数组连接

x = [1, 2, 3]

x.append([4, 5]) # [1, 2, 3, [4, 5]]

x.extend([4, 5]) # [1, 2, 3, 4, 5],注意 extend 返回值为 None

可以使用 pop、slices、del、remove 等移除列表中元素:

myList = [10,20,30,40,50]

# 弹出第二个元素
myList.pop(1) # 20
# myList: myList.pop(1)

# 如果不加任何参数,则默认弹出最后一个元素
myList.pop()

# 使用 slices 来删除某个元素
a = [  1, 2, 3, 4, 5, 6 ]
index = 3 # Only Positive index
a = a[:index] + a[index+1 :]

# 根据下标删除元素
myList = [10,20,30,40,50]
rmovIndxNo = 3
del myList[rmovIndxNo] # myList: [10, 20, 30, 50]

# 使用 remove 方法,直接根据元素删除
letters = ["a", "b", "c", "d", "e"]
numbers.remove(numbers[1])
print(*letters) # used a * to make it unpack you don't have to

Iteration: 索引遍历

你可以使用基本的 for 循环来遍历数组中的元素,就像下面介个样纸:

animals = ['cat', 'dog', 'monkey']
for animal in animals:
    print animal
# Prints "cat", "dog", "monkey", each on its own line.

如果你在循环的同时也希望能够获取到当前元素下标,可以使用 enumerate 函数:

animals = ['cat', 'dog', 'monkey']
for idx, animal in enumerate(animals):
    print '#%d: %s' % (idx + 1, animal)
# Prints "#1: cat", "#2: dog", "#3: monkey", each on its own line

Python 也支持切片(Slices):

nums = range(5)    # range is a built-in function that creates a list of integers
print nums         # Prints "[0, 1, 2, 3, 4]"
print nums[2:4]    # Get a slice from index 2 to 4 (exclusive); prints "[2, 3]"
print nums[2:]     # Get a slice from index 2 to the end; prints "[2, 3, 4]"
print nums[:2]     # Get a slice from the start to index 2 (exclusive); prints "[0, 1]"
print nums[:]      # Get a slice of the whole list; prints ["0, 1, 2, 3, 4]"
print nums[:-1]    # Slice indices can be negative; prints ["0, 1, 2, 3]"
nums[2:4] = [8, 9] # Assign a new sublist to a slice
print nums         # Prints "[0, 1, 8, 9, 4]"

Comprehensions: 变换

Python 中同样可以使用 map、reduce、filter,map 用于变换数组:

# 使用 map 对数组中的每个元素计算平方
items = [1, 2, 3, 4, 5]
squared = list(map(lambda x: x**2, items))

# map 支持函数以数组方式连接使用
def multiply(x):
    return (x*x)
def add(x):
    return (x+x)

funcs = [multiply, add]
for i in range(5):
    value = list(map(lambda x: x(i), funcs))
    print(value)

reduce 用于进行归纳计算:

# reduce 将数组中的值进行归纳

from functools import reduce
product = reduce((lambda x, y: x * y), [1, 2, 3, 4])

# Output: 24

filter 则可以对数组进行过滤:

number_list = range(-5, 5)
less_than_zero = list(filter(lambda x: x < 0, number_list))
print(less_than_zero)

# Output: [-5, -4, -3, -2, -1]

字典类型

创建增删

d = {'cat': 'cute', 'dog': 'furry'}  # 创建新的字典
print d['cat']       # 字典不支持点(Dot)运算符取值

如果需要合并两个或者多个字典类型:

# python 3.5
z = {**x, **y}

# python 2.7
def merge_dicts(*dict_args):
    """
    Given any number of dicts, shallow copy and merge into a new dict,
    precedence goes to key value pairs in latter dicts.
    """
    result = {}
    for dictionary in dict_args:
        result.update(dictionary)
    return result

索引遍历

可以根据键来直接进行元素访问:

# Python 中对于访问不存在的键会抛出 KeyError 异常,需要先行判断或者使用 get
print 'cat' in d     # Check if a dictionary has a given key; prints "True"

# 如果直接使用 [] 来取值,需要先确定键的存在,否则会抛出异常
print d['monkey']  # KeyError: 'monkey' not a key of d

# 使用 get 函数则可以设置默认值
print d.get('monkey', 'N/A')  # Get an element with a default; prints "N/A"
print d.get('fish', 'N/A')    # Get an element with a default; prints "wet"


d.keys() # 使用 keys 方法可以获取所有的键

可以使用 for-in 来遍历数组:

# 遍历键
for key in d:

# 比前一种方式慢
for k in dict.keys(): ...

# 直接遍历值
for value in dict.itervalues(): ...

# Python 2.x 中遍历键值
for key, value in d.iteritems():

# Python 3.x 中遍历键值
for key, value in d.items():

其他序列类型

集合

# Same as {"a", "b","c"}
normal_set = set(["a", "b","c"])
 
# Adding an element to normal set is fine
normal_set.add("d")
 
print("Normal Set")
print(normal_set)
 
# A frozen set
frozen_set = frozenset(["e", "f", "g"])
 
print("Frozen Set")
print(frozen_set)
 
# Uncommenting below line would cause error as
# we are trying to add element to a frozen set
# frozen_set.add("h")

函数

函数定义

Python 中的函数使用 def 关键字进行定义,譬如:

def sign(x):
    if x > 0:
        return 'positive'
    elif x < 0:
        return 'negative'
    else:
        return 'zero'


for x in [-1, 0, 1]:
    print sign(x)
# Prints "negative", "zero", "positive"

Python 支持运行时创建动态函数,也即是所谓的 lambda 函数:

def f(x): return x**2

# 等价于
g = lambda x: x**2

参数

Option Arguments: 不定参数

def example(a, b=None, *args, **kwargs):
  print a, b
  print args
  print kwargs

example(1, "var", 2, 3, word="hello")
# 1 var
# (2, 3)
# {'word': 'hello'}

a_tuple = (1, 2, 3, 4, 5)
a_dict = {"1":1, "2":2, "3":3}
example(1, "var", *a_tuple, **a_dict)
# 1 var
# (1, 2, 3, 4, 5)
# {'1': 1, '2': 2, '3': 3}

生成器

def simple_generator_function():
    yield 1
    yield 2
    yield 3

for value in simple_generator_function():
    print(value)

# 输出结果
# 1
# 2
# 3
our_generator = simple_generator_function()
next(our_generator)
# 1
next(our_generator)
# 2
next(our_generator)
#3

# 生成器典型的使用场景譬如无限数组的迭代
def get_primes(number):
    while True:
        if is_prime(number):
            yield number
        number += 1

装饰器

装饰器是非常有用的设计模式:

# 简单装饰器

from functools import wraps
def decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print('wrap function')
        return func(*args, **kwargs)
    return wrapper

@decorator
def example(*a, **kw):
    pass

example.__name__  # attr of function preserve
# 'example'
# Decorator 

# 带输入值的装饰器

from functools import wraps
def decorator_with_argument(val):
  def decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
      print "Val is {0}".format(val)
      return func(*args, **kwargs)
    return wrapper
  return decorator

@decorator_with_argument(10)
def example():
  print "This is example function."

example()
# Val is 10
# This is example function.

# 等价于

def example():
  print "This is example function."

example = decorator_with_argument(10)(example)
example()
# Val is 10
# This is example function.

类与对象

类定义

Python 中对于类的定义也很直接:

class Greeter(object):
    
    # Constructor
    def __init__(self, name):
        self.name = name  # Create an instance variable
        
    # Instance method
    def greet(self, loud=False):
        if loud:
            print 'HELLO, %s!' % self.name.upper()
        else:
            print 'Hello, %s' % self.name
        
g = Greeter('Fred')  # Construct an instance of the Greeter class
g.greet()            # Call an instance method; prints "Hello, Fred"
g.greet(loud=True)   # Call an instance method; prints "HELLO, FRED!"
# isinstance 方法用于判断某个对象是否源自某个类
ex = 10
isinstance(ex,int)

Managed Attributes: 受控属性

# property、setter、deleter 可以用于复写点方法

class Example(object):
    def __init__(self, value):
       self._val = value
    @property
    def val(self):
        return self._val
    @val.setter
    def val(self, value):
        if not isintance(value, int):
            raise TypeError("Expected int")
        self._val = value
    @val.deleter
    def val(self):
        del self._val
    @property
    def square3(self):
        return 2**3

ex = Example(123)
ex.val = "str"
# Traceback (most recent call last):
#   File "", line 1, in
#   File "test.py", line 12, in val
#     raise TypeError("Expected int")
# TypeError: Expected int

类方法与静态方法

class example(object):
  @classmethod
  def clsmethod(cls):
    print "I am classmethod"
  @staticmethod
  def stmethod():
    print "I am staticmethod"
  def instmethod(self):
    print "I am instancemethod"

ex = example()
ex.clsmethod()
# I am classmethod
ex.stmethod()
# I am staticmethod
ex.instmethod()
# I am instancemethod
example.clsmethod()
# I am classmethod
example.stmethod()
# I am staticmethod
example.instmethod()
# Traceback (most recent call last):
#   File "", line 1, in
# TypeError: unbound method instmethod() ...

对象

实例化

属性操作

Python 中对象的属性不同于字典键,可以使用点运算符取值,直接使用 in 判断会存在问题:

class A(object):
    @property
    def prop(self):
        return 3

a = A()
print "'prop' in a.__dict__ =", 'prop' in a.__dict__
print "hasattr(a, 'prop') =", hasattr(a, 'prop')
print "a.prop =", a.prop

# 'prop' in a.__dict__ = False
# hasattr(a, 'prop') = True
# a.prop = 3

建议使用 hasattr、getattr、setattr 这种方式对于对象属性进行操作:

class Example(object):
  def __init__(self):
    self.name = "ex"
  def printex(self):
    print "This is an example"


# Check object has attributes
# hasattr(obj, 'attr')
ex = Example()
hasattr(ex,"name")
# True
hasattr(ex,"printex")
# True
hasattr(ex,"print")
# False

# Get object attribute
# getattr(obj, 'attr')
getattr(ex,'name')
# 'ex'

# Set object attribute
# setattr(obj, 'attr', value)
setattr(ex,'name','example')
ex.name
# 'example'

异常与测试

异常处理

Context Manager – with

with 常用于打开或者关闭某些资源:

host = 'localhost'
port = 5566
with Socket(host, port) as s:
    while True:
        conn, addr = s.accept()
        msg = conn.recv(1024)
        print msg
        conn.send(msg)
        conn.close()

单元测试

from __future__ import print_function

import unittest

def fib(n):
    return 1 if n<=2 else fib(n-1)+fib(n-2)

def setUpModule():
        print("setup module")
def tearDownModule():
        print("teardown module")

class TestFib(unittest.TestCase):

    def setUp(self):
        print("setUp")
        self.n = 10
    def tearDown(self):
        print("tearDown")
        del self.n
    @classmethod
    def setUpClass(cls):
        print("setUpClass")
    @classmethod
    def tearDownClass(cls):
        print("tearDownClass")
    def test_fib_assert_equal(self):
        self.assertEqual(fib(self.n), 55)
    def test_fib_assert_true(self):
        self.assertTrue(fib(self.n) == 55)

if __name__ == "__main__":
    unittest.main()

存储

文件读写

路径处理

Python 内置的 __file__ 关键字会指向当前文件的相对路径,可以根据它来构造绝对路径,或者索引其他文件:

# 获取当前文件的相对目录
dir = os.path.dirname(__file__) # src\app

## once you're at the directory level you want, with the desired directory as the final path node:
dirname1 = os.path.basename(dir) 
dirname2 = os.path.split(dir)[1] ## if you look at the documentation, this is exactly what os.path.basename does.

# 获取当前代码文件的绝对路径,abspath 会自动根据相对路径与当前工作空间进行路径补全
os.path.abspath(os.path.dirname(__file__)) # D:\WorkSpace\OWS\tool\ui-tool-svn\python\src\app

# 获取当前文件的真实路径
os.path.dirname(os.path.realpath(__file__)) # D:\WorkSpace\OWS\tool\ui-tool-svn\python\src\app

# 获取当前执行路径
os.getcwd()

可以使用 listdir、walk、glob 模块来进行文件枚举与检索:

# 仅列举所有的文件
from os import listdir
from os.path import isfile, join
onlyfiles = [f for f in listdir(mypath) if isfile(join(mypath, f))]

# 使用 walk 递归搜索
from os import walk

f = []
for (dirpath, dirnames, filenames) in walk(mypath):
    f.extend(filenames)
    break

# 使用 glob 进行复杂模式匹配
import glob
print(glob.glob("/home/adam/*.txt"))
# ['/home/adam/file1.txt', '/home/adam/file2.txt', .... ]

简单文件读写

# 可以根据文件是否存在选择写入模式
mode = 'a' if os.path.exists(writepath) else 'w'

# 使用 with 方法能够自动处理异常
with open("file.dat",mode) as f:
    f.write(...)
    ...
    # 操作完毕之后记得关闭文件
    f.close()

# 读取文件内容
message = f.read()

复杂格式文件

JSON

import json

# Writing JSON data
with open('data.json', 'w') as f:
     json.dump(data, f)

# Reading data back
with open('data.json', 'r') as f:
     data = json.load(f)

XML

我们可以使用 lxml 来解析与处理 XML 文件,本部分即对其常用操作进行介绍。lxml 支持从字符串或者文件中创建 Element 对象:

from lxml import etree

# 可以从字符串开始构造
xml = '<a xmlns="test"><b xmlns="test"/></a>'
root = etree.fromstring(xml)
etree.tostring(root)
# b'<a xmlns="test"><b xmlns="test"/></a>'

# 也可以从某个文件开始构造
tree = etree.parse("doc/test.xml")

# 或者指定某个 baseURL
root = etree.fromstring(xml, base_url="http://where.it/is/from.xml")

其提供了迭代器以对所有元素进行遍历:

# 遍历所有的节点
for tag in tree.iter():
    if not len(tag):
        print tag.keys() # 获取所有自定义属性
        print (tag.tag, tag.text) # text 即文本子元素值

# 获取 XPath
for e in root.iter():
    print tree.getpath(e)

lxml 支持以 XPath 查找元素,不过需要注意的是,XPath 查找的结果是数组,并且在包含命名空间的情况下,需要指定命名空间:

root.xpath('//page/text/text()',ns={prefix:url})

# 可以使用 getparent 递归查找父元素
el.getparent()

lxml 提供了 insert、append 等方法进行元素操作:

# append 方法默认追加到尾部
st = etree.Element("state", name="New Mexico")
co = etree.Element("county", name="Socorro")
st.append(co)

# insert 方法可以指定位置
node.insert(0, newKid)

Excel

可以使用 [xlrd]() 来读取 Excel 文件,使用 xlsxwriter 来写入与操作 Excel 文件。

# 读取某个 Cell 的原始值
sh.cell(rx, col).value
# 创建新的文件
workbook = xlsxwriter.Workbook(outputFile)
worksheet = workbook.add_worksheet()

# 设置从第 0 行开始写入
row = 0

# 遍历二维数组,并且将其写入到 Excel 中
for rowData in array:
    for col, data in enumerate(rowData):
        worksheet.write(row, col, data)
    row = row + 1

workbook.close()

文件系统

对于高级的文件操作,我们可以使用 Python 内置的 shutil

# 递归删除 appName 下面的所有的文件夹
shutil.rmtree(appName)

网络交互

Requests

Requests 是优雅而易用的 Python 网络请求库:

import requests

r = requests.get('https://api.github.com/events')
r = requests.get('https://api.github.com/user', auth=('user', 'pass'))

r.status_code
# 200
r.headers['content-type']
# 'application/json; charset=utf8'
r.encoding
# 'utf-8'
r.text
# u'{"type":"User"...'
r.json()
# {u'private_gists': 419, u'total_private_repos': 77, ...}

r = requests.put('http://httpbin.org/put', data = {'key':'value'})
r = requests.delete('http://httpbin.org/delete')
r = requests.head('http://httpbin.org/get')
r = requests.options('http://httpbin.org/get')

数据存储

MySQL

import pymysql.cursors

# Connect to the database
connection = pymysql.connect(host='localhost',
                             user='user',
                             password='passwd',
                             db='db',
                             charset='utf8mb4',
                             cursorclass=pymysql.cursors.DictCursor)

try:
    with connection.cursor() as cursor:
        # Create a new record
        sql = "INSERT INTO `users` (`email`, `password`) VALUES (%s, %s)"
        cursor.execute(sql, ('webmaster@python.org', 'very-secret'))

    # connection is not autocommit by default. So you must commit to save
    # your changes.
    connection.commit()

    with connection.cursor() as cursor:
        # Read a single record
        sql = "SELECT `id`, `password` FROM `users` WHERE `email`=%s"
        cursor.execute(sql, ('webmaster@python.org',))
        result = cursor.fetchone()
        print(result)
finally:
    connection.close() 
「真诚赞赏,手留余香」

初学机器学习的你,是否掌握了这样的Linux技巧?

随着软件系统的不断发展,今天,不同的操作系统对应着不同的适用人群:Windows 面向办公室和商用,Mac 面向创意人群,而 Linux 面向软件开发者。对于操作系统提供商而言,这种市场分割大幅度简化了产品技术需求、用户体验和产品方向上的投入。然而,这也加剧了兼容性问题,让不同业务进入了狭窄、互不相容的领域:商务人士无法对创意提供洞察力,而开发者也无法深入到商务决策中去。

在现实中,知识和技能是流动的,跨越多个学科和领域。与其说「你只能擅长一件事」的理念是迈向精通的路线图,还不如说一种过早优化的方法。一旦从社会中采样大量的任务,你就只能知道你擅长什么,也许你还发现自己擅长它们中的很多。

对于现代的业务分析师,弥补业务与软件之间的鸿沟尤其重要。业务分析必须是「双重平台」,能够利用仅在 Linux(或 OS X)上可用的命令行工具,但是仍然受益于 Windows 的 Microsoft Office。可以理解的是,Linux 会使具有商学学位的人感到恐惧。幸运的是,正如大多数事情一样,你只需 20% 的任务即可完成 80% 的工作。下面是我的 20%。

业务分析是基于数据的,而机器学习正是强大的数据分析工具。我们利用机器学习模型分析数据最好的环境却恰恰是 Linux 系统,这不仅是因为它支持广泛的 Python 机器学习库,同时在于环境配置与管理的简单明了。因此,本文将为机器学习读者梳理 Linux 系统的基本特性与命令。

为什么机器学习分析师需要了解 Linux

由于其开源的底层,Linux 从不断从数以万计的开发者贡献中受益。他们构建的程序和工具不仅使其工作更简单,也简化了跟随他们的编程人员的工作。结果,开源开发带来了一种网络效应:在平台上构建工具的开发者越多,能够利用这些工具立刻编写其程序的其他开发者就越多。

结果就是 Linux 中编写的 Linux 程序和实用工具(统称为软件)的扩展套件——其中很多从未用于 Windows。一个示例是被称作 git 的流行的版本控制系统(VCS)。开发者本可以编写这一在 Windows 工作的软件,但是却没有。他们让其在 Linux 命令行上工作,因为生态系统已经提供全部所需的工具。

具体来说,Windows 上的开发有两个主要问题:

1. 基本任务,比如文件解析、工作调度和文本搜索比运行命令行工具更为重要。

2. 编程语言(比如 Python、C++)及其相关代码库会引发错误,因为它们期望特定的 Linux 参数或文件系统定位。

这意味着若想在 Windows 上进行开发,我们需要花费更多的时间来重写 Linux 中已有的基本工具,并排除操作系统兼容性错误。这并不令人意外——Windows 生态系统当初并没有考虑软件开发设计的需求。

借助这个 Linux 开发案例,让我们从最基本的开始。

Linux 的基本单元:「shell」

「shell」(也被称为终端、控制台或命令行)是一个基于文本的用户界面,通过它把命令发送给机器。在 Linux 中,shell 的默认语言是 bash。与主要在 Windows 内部进行点击操作的 Windows 用户不同,Linux 开发者坚持使用键盘把命令输入到 shell。对于那些没有编程背景的人来说,这种转变一开始也许会不自然,但是在 Linux 中开发的好处很容易超过最初的学习投资。

学习几个重要的概念

和成熟的编程语言相比,bash 只需要学习几个主要的概念。这一步完成之后,之后 bash 的学习就只剩下记忆了。更清楚地说就是:要学好 bash,只需要记住 20—30 个命令(command)以及其中最常用的参数(argument)就可以了。

对于非开发者而言,Linux 很令人费解,因为开发者似乎能随意且不费力地使用深奥的终端命令。其实是因为他们只记住了少量的命令—对于更复杂的问题,他们(和所有普通人一样)也需要谷歌一下。

以下就是 bash 中的主要概念。

命令语法

bash 中的命令是区分大小写的,且遵循 {命令}{参数} 的语法结构。

例如,在『grep-inr』中,grep 是命令(搜索文本的一个字符串),-inr 是标记(flag)或参数(随 grep 默认运行而变化)。理解这个命令的唯一方法是使用谷歌搜索,或输入『man grep』命令。我推荐同时学习命令和其中最常用的参数,否则单独学习每一个标记的作用是很费力的。

目录相对地址

当前目录:.

上一级目录的上一级目录:..

用户的主目录:~

文件的系统根目录:/

例如,为了从当前目录换到上一级目录,需要输入:「cd..」。类似地,为了复制位于「/path/to/file.txt」文件到上一级目录中,需要输入「cp /path/to/file.txt.」(请注意命令末尾的点)。这些例子中使用的都是相对路径,可以使用绝对路径替换。

标准输入(STDIN)/标准输出(STDOUT)

任何输入和提交(通过键入 ENTER)到窗口的命令都被称为标准输入(standard input,STDIN)。

任何程序打印(print)到终端的东西(例如,一份文件中的文本)都被称为标准输出(standard output,STDOUT)。

管道(PIPING)

1 |

一种管道,其左方是一个命令的 STNOUT,将作为管道右方的另一个命令的 STDIN。

例如:echo ‘test text’ | wc -l

2 >

大于号,作用是取一个命令 STDOUT 位于左方,并将其写入/覆写(overwrite)入右方的一个新文件。

例如:ls > tmp.txt

3 >>

两个大于号,作用是取一个命令 STDOUT 位于左方,并将其追加到右方的一个新的或现有文件中。

例如:date >> tmp.txt

通配符(WILDCARDS)

这类似于 SQL 中的% 符号,例如,使用「WHERE first_name LIKE 『John%』」搜索所有以 John 起始的名字。

在 bash 中,相应的命令是「John*」。如果想列出一个文件夹中所有以「.json」结尾的文件,可以输入:「ls *.json」。

TAB 键自动完成

如果我们输入一个命令并按下 TAB 键,那么 Bash 将自动完成该命令。但是,我们也应该使用一些如 zsh 或 fish 工具来自动完成,因为我们很难记住各种命令及它们的参数。更准确地说,这些工具会基于我们的命令行历史自动完成命令语句。

退出

有时候我们会卡在一些程序中并不知道如何退出它们。这在 Linux 新手中是很常见的问题,这也会大大损害新手的积极性。一般来说,退出命令会和字母「q」有一些关系,所以记住以下的退出命令或快捷键就十分有用了。

  • Bash

CTRL+c

q

exit

  • Python

quit()

CTRL+d

  • Nano: CTRL+x
  • Vim: <Esc> :q!

常用 Bash 命令

以下是在 Linux 中最常用到的指令,在使用新系统进行开发时,记住这些指令对于快速上手非常重要。

  • cd {directory}:转换当前目录
  • ls -lha:列出目录文件(详细信息)
  • vim or nano:命令行编辑器
  • touch {file}:创建一个新的空文件
  • cp -R {original_name} {new_name}:复制一个文件或目录(包含内部所有文件)
  • mv {original_name} {new_name}:移动或重命名文件
  • rm {file}:删除文件
  • rm -rf {file/folder}:永久删除文件或文件夹(小心使用)
  • pwb:打印当前工作目录
  • cat or less or tail or head -n10 {file}:文件的标准输出内容
  • mkdir {directory}:创建一个空的目录
  • grep -inr {string}:在当前目录或子目录的文件中搜索一个字符串
  • column -s, -t <delimited_file>:在 columnar 格式中展示逗号分隔文件
  • ssh {username}@{hostname}:连接到远程机器中
  • tree -LhaC 3:向下展示三级目录结构(带有文件大小信息和隐藏目录信息)
  • htop (or top):任务管理器
  • pip install –user {pip_package}:Python 安装包管理器,安装包到~/.local/bin 目录下
  • pushd . ; popd ; dirs; cd -:在堆栈上 push/pop/view 一个目录,并变回最后一个目录
  • sed -i “s/{find}/{replace}/g” {file}:替代文件中的一个字符串
  • find . -type f -name ‘*.txt’ -exec sed -i “s/{find}/{replace}/g” {} \;:替换当前目录和子目录下后缀名为.txt 文件的一个字符串
  • tmux new -s session, tmux attach -t session:创建另一个终端会话界面而不创建新的窗口 [高级命令]
  • wget {link}:下载一个网页或网页资源
  • curl -X POST -d “{key: value}” http://www.google.com:发送一个 HTTP 请求到网站服务器
  • find <directory>:递归地列出所有目录和其子目录的内容

高级 & 不常用的指令

保留一个有用命令列表以备不需也是非常必要的,即使这些情况不常发生(如某个进程阻塞了几个网络端口)。以下我们将列出几个不常用命令:

  • lsof -i :8080:列出打开文件的描述符(-i 是网络接口的标记)
  • netstat | head -n20:列出当前打开的 Internet/UNIX 接口(socket)以及相关信息
  • dstat -a:输出当前硬盘、网络、CPU 活动等信息
  • nslookup <IP address>:找到远程 IP 地址的主机名
  • strace -f -e <syscall> <cmd>:跟踪程序的系统调用(-e 标记用于过滤某些系统调用)
  • ps aux | head -n20:输出目前活动的进程
  • file <file>:检查文件类型(例如可执行文件、二进制文件、ASCII 文本文件)
  • uname -a:内核信息
  • lsb_release -a:系统信息
  • hostname:检视你的机器的主机名(即其他电脑可以搜索到的名称)
  • pstree:可视化分支进程
  • time <cmd>:执行一个命令并报告用时
  • CTRL + z ; bg; jobs; fg:从当前 tty 中传递一个进程到后台再返回前台
  • cat file.txt | xargs -n1 | sort | uniq -c:统计文件中的独特字(unique words)数量
  • wc -l <file>:计算文件的行数
  • du -ha:在磁盘上显示目录及其内容的大小
  • zcat <file.gz>:显示压缩文本文件的内容
  • scp <user@remote_host> <local_path>:将文件从远端复制到本地服务器,或反过来
  • man {command}:为一个命令显示 manual(说明文档),但是通常这样不如谷歌搜索好用

原文链接:http://alexpetralia.com/posts/2017/6/26/learning-linux-bash-to-get-things-done

后端服务性能压测实践

背景

最近大半年内有过两次负责性能压测的一些工作。一件事情做了一次可能还无法总结出一些东西,两次过后还是能发现一些共性问题,所以总结下性能压测的一般性实践。但是问题肯定不止这些,还有更多深层次的问题等着发现,等我们遇到了在逐个解决再来总结分享。

做性能压测的原因就不多说了,一般两个时间点是必须要做的,大促前、新系统上线。压测都是为了系统在线上的处理能力和稳定性维持在一个标准范围内,做到心中有数。

从整个行业来看,抛开一些大厂不说,全自动化的性能压测环境还是比较少的,要想建设好一套全自动化的性能压测环境起码涉及到几个问题,CI\CD、独立、隔离的压测环境,自动化压测工具、日常压测性能报警、性能报表分析、排查/解决性能问题流程等等。这样才能将性能压测常规化,一旦不是常规化性能压测,就会有代码、中间件配置滞后于生产环境的问题。时间一长,就等于要重新开始搭建、排查压测环境。

如果性能压测的环境是全自动化的,那么就可以把性能压测工作常规化变成研发过程中的一个例行的事项,执行起来效率就会非常高,压测的时候也会比较轻松,好处也是比较明显的。

但是大多数的时候我们还是需要从零开始进行性能压测的工作。毕竟搭建这样一套环境给企业带来的成本也是巨大的。性能压测对环境敏感,必须划分独立的部署、隔离单元,才能在后续的常规压测流程中直观的阅读压测报告。

题外话,如果有了自动化的压测环境,也还是需要去了解下整个压测环境的基本架构,毕竟压测环境不是真实的生产环境,有些问题我们需要知道是正常的还是不正常的。

环境检测

当我们需要进行性能压测时首先要面对的问题就是环境问题,环境问题包含了常见的几个点:

1.机器问题(实体机还是虚拟机、CPU、内存、网络适配器进出口带宽、硬盘大小,硬盘是否 SSD、内核基本参数配置)

2.网络问题(是否有跨网段问题、网段是否隔离、如果有跨网段机器,是否能访问、跨网段是否有带宽限速)

3.中间件问题(程序里所有依赖的中间件是否有部署,中间件的配置是否初始化、中间件 cluster 结构什么样、这些中间件是否都进行过性能压测、压测的纬度是什么,是 benchmark 还是针对特定业务场景的压测)

这些环境问题第一次排查的时候会有点累,但是掌握了一些方法、工具、流程之后剩下的也就是例行的事情,只不过人工参与的工作多点。

上面的问题里,有些问题查看是比较简单的,这里就不介绍了,比如机器的基本配置等。有些配置只需要你推动下,走下相关流程回头验收下,比如网段隔离等,也还是比较简单的。

比较说不清楚的是中间件问题,看上去都是能用的,但是就是压不上去,这时候就需要你自己去进行简单的压测,比如 db 的单表插入、cache 的并发读取、mq 的落地写入等。这时候就涉及到一个问题,你需要对这些中间件都有一定深度的了解,要知道内在的运行机制,要不然出现异常情况排查起来确实很困难。

其实没有人能熟悉市面上所有的中间件,每一个中间件都很复杂,我们也不可能掌握一个中间件的所有点,但是常用的一些我们是需要掌握的,至少知道个大概的内部结构,可以顺藤摸瓜的排查问题。

但是事实上总有你不熟悉的,这个时候求助下大家的力量互相探讨再自己摸索找点资料,我们没遇到过也许别人遇到过,学技术其实就是这么个过程。

压力机及压力工具检测

既然做性能压测就需要先对压测机、压力工具先进行了解,压测工具我们主要有 locustjmeterab,前两者主要是压测同事进行准出验收测试使用的。

后两者主要是用来提交压测前的自检使用,就是开发自己用来检查和排错使用的。这里需要强调下 ab 其实是做基准测试的,不同于 jmeter 的作用。

需要知道压力机是否和被压测机器服务器在一个网段,且网段之间没有任何带宽限制。压力机的压测工具配置是否有瓶颈,一般如果是 jmeter 的话需要检查 java 的一些基本配置。

但是一般如果压力机是固定不变的,一直在使用的,那么基本不会有什么问题,因为压力机压测同事一直维护者,反而是自己使用的压测工具的参数要做好配置和检测。

jmeter 压测的时候,如果压测时间过长,记得关掉 监听器->图形结果 面板,因为那个渲染如果时间太长基本会假死,误以为会是内存的问题,其实是渲染问题。

在开发做基准压测的时候有一个问题就是办公网络与压测服务器的网络之间的带宽问题,压力过大会导致办公网络出现问题。所以需要错开时间段。

大致梳理好后,我们需要通过一些工具来查看下基本配置是否正常。比如,ethtool 网络适配器信息、nload 流量情况等等,当然还有很多其他优秀的工具用来查看各项配置,这里就不罗列了。

使用 ethtool 查看网络适配器信息前需要先确定当前机器有几个网络适配器,最好的办法是使用 ifconfig找到你正在使用的网络适配器。

排除 127.0.0.1 的适配器外,还有三个适配器信息,只有第一个 bond0 才是我们正在使用的,然后使用 ethtool 查看当前 bond0 的详细适配器信息。重点关注下 speed 域,它表示当前网络适配器的带宽。

虽然网络适配器可能配置的没有问题,但是整个网络是否没问题还需要咨询相关的运维同事进行排查下,中间还可能存在限速问题。

要确定网络带宽确实没有问题,我们还需要一个实时的监控网络流量工具,这里我们使用nload来监控下进出口流量问题。

这个工具还是很不错的,尤其是在压测的过程中可以观察流量的进出口情况,尤其是排查一些间隙抖动情况。

如果发现进口流量一直很正常,出口流量下来了有可能系统对外调用再放慢,有可能是下游调用 block,但是 request 线程池还未跑满,也有可能内部是纯 asyncrequest 线程根本不会跑满,也有可能是压测工具本身的压力问题等等。但是我们至少知道是自己的系统对外调用这个边界出了问题。

Linux openfiles limit 设置

工作环境中,一般情况下 linux 打开文件句柄数上限是不需要我们设置的,这些初始化的值运维同事一般是设置过的,而且是符合运维统一标准的。但是有时候关于最大连接数设置还要根据后端系统的使用场景来决定。

以防万一我们还是需要自己检查下是否符合当前系统的压测要求。

Linux 中一切都是文件,socket 也是文件,所以需要查看下当前机器对于文件句柄打开的限制,查看 ulimit -aopen files 域,也可以直接查看ulimit -n

如果觉得配置的参数需要调整,可以通过编辑 /etc/security/limits.conf 配置文件。

排查周边依赖

要想对一个服务进行压测,就需要对这个服务周边依赖进行一个排查,有可能你所依赖的服务不一定具备压测条件。并不是每个系统的压测都在一个时间段内,所以你在压测的时候别人的服务也许并不需要压测等等。

还有类似中间件的问题,比如,如果我们依赖中间件 cache ,那么是否有本地一级 cache ,如果有的话也许对压测环境的中间件 cache 依赖不是太大。如果我们依赖中间件 mq ,是不是在业务上可以断开对 mq的依赖,因为我们毕竟不是对 mq 进行压测。还有我们所依赖服务也不关心我们的压测波动。

整理出来之后最好能画个草图,再重新 git branch -b 重新拉一个性能压测的 branch 出来根据草图进行调整代码依赖。然后压测的时候观察流量和数据的走向,是否符合我们梳理之后的路线。

空接口压测检测

为了快速验证压测服务一个简单的办法,就是通过压测一个空接口,查看下整个网络是否通畅,各个参数是否大体上正常。

一般在任何一个后端服务中,都有类似 health_checkendpoint,方便起见可以直接找一个没有任何下游依赖的接口进行压测,这类接口主要是为了验证服务器的 onlineoffline 状态。

如果当前服务没有类似 health_check 新建一个空接口也可以,而且实践证明,一个服务在生产环境非常需要这么一个接口,必要情况下可以帮助来排查调用链路问题。

《发布!软件的设计与部署》Jolt 大奖图书 第17章 透明性 介绍了架构的透明性设计作用。

聚合报告中 throughput 计算

我们在用 jmeter 进行压测的时候关于 聚合报告 中的 throughput 理解需要统一下。

正常情况下在使用 jmeter 压测的时候会仔细观察 throughput 这一列的变化情况,但是没有搞清楚 thourghput 的计算原理的时候就会误以为是 tps/qps 下来了,其实有时候是整个远程服务器根本就没有 response 了。

throughput=samples/压测时间

throughput(吞吐量) 是单位时间内的请求处理数,一般是按 second 计算,如果是压测 write 类型的接口,那么就是 tps 指标。如果压测 read 类型的接口,那么就是 qps 指标。这两种类型的指标是完全不一样的,我们不能搞混淆了。

200(throughput) tps=1000(write)/5(s)1000(throughput) qps=2000(read)/2(s)

当我们发现 throughput 逐渐下来的时候要考虑一个时间的纬度。

也就是说我们的服务有可能已经不响应了,但是随着压测时间的积累,整个吞吐量的计算自然就在缓慢下滑,像这种刺尖问题是发现不了的。

这一点用ui版本的 jmeter 尤其明显,因为它的表现方式就是在欢欢放慢。用 Linux 版本的 jmeter 还好点,因为它的输出打印是隔断时间才打印。

关于这个点没有搞清楚非常影响我们对性能压测的结果判断。所以我们在压测的时候一定要有监控报表,才能知道在整个压测过程中服务器的各项指标是否出现过异常情况。

大多数的时候我们还会使用 apache ab 做下基本的压测,主要是用来与 jmeter 对比下,两个工具压测的结果是否相差不大,主要用来纠偏一些性能虚高问题。

apache abjmeter 各有侧重,ab 可以按固定请求数来压,jmeter 可以按时间来压,最后计算的时候需要注意两者区别。ab 好像是没有请求错误提示和中断的,jmeter 是有错误提示,还有各个纬度断言设置。

我们在使用压测工具的时候,大致了解下工具的一些原理有助于准确的使用这款工具。

压测及性能排查方法

在文章的前面部分讲到了 排查周边依赖 的环境检查步骤。其实要想顺利的进行压测,这一步是必须要有的。经过这一步分析我们会有一个基本的 系统依赖 roadmap

基于这份 系统依赖 roadmap 我们将进行性能压测和问题定位及性能优化。

合理的系统架构应该是上层依赖下层,在没有确定下游系统性能的情况下,是没办法确定上游系统性能的瓶颈在哪里。

所以压测的顺序应该尽可能的从下往上依次进行,这样可以避免无意义的排查由于下游吞吐量不够带来的性能问题。越是下游系统性能要求越高,因为上游系统的性能瓶颈直接依赖下游系统。

比如,商品系统的 v1/product/{productid} 前台接口,吞吐量为 qps 8000,那么所有依赖这个接口的上游服务在这个代码路径上最高吞吐量瓶颈就是 8000 ,代码路径不管是 tps 还是 qps 都是一样的瓶颈。

上层服务可以使用 async方式来提高 request 并发量,但是无法提高代码路径在 v1/product/{productid} 业务上的吞吐量。

我们不能将并发和吞吐量搞混淆了,系统能扛住多少并发不代表吞吐量就很高。可以有很多方式来提高并发量,threadpool 提高线程池大小 、socket 类c10k 、nio事件驱动,诸如此类方法。

关注各纬度 log

当在压测的过程中定位性能问题的性价比较高的方法就是请求处理的log,请求处理时长log,对外接口调用时长log,这一般能定位大部分比较明显的问题。当我们用到了一些中间件的时候都会输出相应的执行log。

如下所示,在我们所使用的开发框架中支持了很多纬度的执行log,这在排查问题的时候就会非常方便。

slow.log 类型的慢日志还是非常有必要记录下来的,这不仅在压测的时候需要,在生产上我们也是非常需要。

如果我们使用了各种中间件,那就需要输出各种中间件的处理日志,mq.logcache.logsearch.log 诸如此类。

除了这些 log 之外,我们还需要重点关注运行时的 gc log

我们主要使用 Java 平台,在压测的时候关注 gc log 是正常的事。哪怕不是 Java 程序,类似基于 vm 的语言都需要关注 gc log 。根据 jvm gcer 配置的不同,输出的日志也不太一样。

一般电商类的业务,以响应为优先时 gc 主要是使用 cms+prenew ,关注 full gc 频次,关注 cms 初始标记并发标记重新标记并发清除 各个阶段执行时间, gc 执行的 real timepernew 执行时的内存回收大小等 。

java gc 比较复杂涉及到的东西也非常多,对 gc log 的解读也需要配合当前的内存各个代的大小及一系列 gc 的相关配置不同而不同。

《Java性能优化权威指南》 java之父gosling推荐,可以长期研究和学习。

Linux 常规命令

在压测的过程中为了能观察到系统的各项资源消耗情况我们需要借助各种工具来查看,主要包括网络、内存、处理器、流量。

netstat

主要是用来查看各种网络相关信息。

比如,在压测的过程中,通过 netstat wc 看下 tcp 连接数是否和服务器 threadpool 设置的匹配。

netstat -tnlp | grep ip | wc -l

如果我们服务器的 threadpool 设置的是50,那么可以看到 tcp 连接数应该是50才对。然后再通过统计 jstack 服务器的 request runing 状态的线程数是不是>=50。

request 线程数的描述信息可能根据使用的 nio 框架的不同而不同。

还有使用频率最高的查看系统启动的端口状态、tcp 连接状态是 establelished 还是 listen 状态。

netstat -tnlp

再配合 ps 命令查看系统启动的状态。这一般用来确定程序是否真的启动了,如果启动了是不是 listen 的端口与配置中指定的端口不一致。

ps aux | grep ecm-placeorder

netstat 命令很强大有很多功能,如果我们需要查看命令的其他功能,可以使用man netstat 翻看帮助文档。

vmstat

主要用来监控虚拟处理器的运行队列统计信息。

vmstat 1

在压测的时候可以每隔 1s2s 打印一次,可以查看处理器负载是不是过高。procsr 子列就是当前处理器的处理队列,如果这个值超高当前 cpu core 数那么处理器负载将过高。可以和下面将介绍的 top 命令搭配着监控。

同时此命令可以在处理器过高的时候,查看内存是否够用是否出现大量的内存交换,换入换出的量多少 swap si 换入 swap so 换出。是否有非常高的上下文切换 system cs 每秒切换的次数,system us 用户态运行时间是否很少。是否有非常高的 io wait 等等。

关于这个命令网上已经有很多优秀的文章讲解,这里就不浪费时间重复了。同样可以使用 man vmstat 命令查看各种用法。

mpstat

主要用来监控多处理器统计信息

mpstat -P ALL 1

我这是一个 32 core 的压测服务器,通过 mpstat 可以监控每一个虚拟处理器的负载情况。也可以查看总的处理器负载情况。

mpstat 1

可以看到 %idle 处于闲置状态的 cpu 百分比,%user 用户态任务占用的 cpu 百分比,%sys系统态内核占用 cpu 百分比,%soft 软中断占用 cpu 百分比,%nice 调整任务优先级占用的 cpu 百分比等等。

iostat

主要用于监控io统计信息

iostat 1

如果我们有大量的 io 操作的话通过 iostat 监控 io 的写入和读取的数据量,同时也能看到在 io 负载特别大的情况下 cpu 的平均负载情况。

top

监控整个系统的整体性能情况top 命令是我们在日常情况下使用频率最高的,可以对当前系统环境了如指掌。处理器 load 率情况,memory 消耗情况,哪个 task 消耗 cpumemory最高。

top

top 命令功能非常丰富,可以分别根据 %MEM%CPU 排序。

load average 域表示 cpu load 率情况,后面三段分别表示最近1分钟、5分钟、15分钟的平均 load 率。这个值不能大于当前 cpu core 数,如果大于说明 cpu load 已经严重过高。就要去查看是不是线程数设置的过高,还要考虑这些任务是不是处理时间太长。设置的线程数与任务所处理的时长有直接关系。

Tasks 域表示任务数情况,total 总的任务数,running 运行中的任务数,sleeping 休眠中的任务数,stopped 暂停中的任务数,zombie 僵尸状态任务数。

Swap 域表示系统的交换区,压测的时候关注 used 是否会持续升高,如果持续升高说明物理内存已经用完开始进行内存页的交换。

free

查看当前系统的内存使用情况

free -m

total 总内存大小,used 已经分配的内存大小,free 当前可用的内存大小,shared 任务之间的共享内存大小,buffers 系统已经分配但是还未使用的,用来存放文件 matedata 元数据内存大小,cached 系统已经分配但是还未使用的,用来存放文件的内容数据的内存大小。

-/+buffer/cache

used 要减去 buffers/cached ,也就是说并没有用掉这么多内存,而是有一部分内存用在了 buffers/cached 里。

free 要加上 buffers/cached ,也就是说还有 buffers/cached 空余内存需要加上。

Swap 交换区统计,total 交换区总大小,used 已经使用的交换区大小,free 交换区可用大小。只需要关注 used 已经使用的交换区大小,如果这里有占用说明内存已经到瓶颈。

《深入理解LINUX内核》、《LINUX内核设计与实现》可以放在手边作为参考手册遇到问题翻翻。

性能排查两种方式(从上往下、从下往上)

当系统出现性能问题的时候可以从两个层面来排查问题,从上往下、从下网上,也可以综合运用这两种方法,压测的时候可以同时查看这两个纬度的信息。

一边打开 topfree 观察 cpumemory 的系统级别的消耗情况,同时一边在通过 jstackjstat 之类的工具查看应用程序运行时的内部状态来综合定位。

总结

本篇文章主要还是从抛砖引玉的角度出发,整理下我们在做一般性能压测的时候出现的常规问题及排查方法和处理流程,并没有多么高深的技术点。

性能问题一旦出现也不会是个简单的问题,都需要花费很多精力来排查问题,运用各种工具、命令来逐步排查,而这些工具和命令所输出的信息都是系统底层原理,需要逐一去理解和实验的,并没有一个银弹能解决所有问题。

 

Clean Code 阅读总结

1 开始

本文是在阅读 clean code 时的一些总结,原书是基于 Java 的,这里将其中的一些个人认为实用性较强且容易与日常业务开发结合的一些原则重新进行整理,并参考了 clean-code-javascript 一文给出了一些代码实例,希望本文能够给日常开发编码和重构作出一些参考。

2 有意义的命名

2.1 名副其实

变量取名要花心思想想,不要贪图方便,过于简略的名称,时间长了以后就难以读懂。

// bad
var d = 10;
var oVal = 20;
var nVal = 100;


// good
var days = 10;
var oldValue = 20;
var newValue = 100;

2.2 避免误导

命名不要让人对变量的信息 (类型,作用) 产生误解。

accounts 和 accountList,除非 accountList 真的是一个 List 类型,否则 accounts 会比 accountList 更好。因此像 List,Map 这样的后缀,不要随意使用。

// bad
var platformList = {
    web: {},
    wap: {},
    app: {},
};


// good
var platforms = {
    web: {},
    wap: {},
    app: {},
};

2.3 做有意义的区分

用明确的意义去表述变量直接的区别。

很多情况下,会有存在 product,productData,productInfo 之类的命名,Data 和 Info 很多情况下并没有明显的区别,不如直接就使用 product。

// bad
var goodsInfo = {
    skuDataList: [],
};

function getGoods(){};          // 获取商品列表
function getGoodsDetail(id){};  // 通过商品ID获取单个商品


// good
var goods = {
    skus: [],
};

function getGoodsList(){};      // 获取商品列表
function getGoodsById(id){};    // 通过商品ID获取单个商品

2.4 使用读得出来的名称

缩写要有个度,比如像 DAT 这样的写法,到底是 DATA 还是 DATE…

// bad
var yyyyMMddStr = eu.format(new Date(), 'yyyy-MM-dd');
var dat = null;
var dev = 'Android';


// good
var todaysDate = eu.format(new Date(), 'yyyy-MM-dd');
var data = null;
var device = 'Android';

2.5 使用可搜索的名称

可搜索的名称能够帮助快速定位代码,尤其对于一些数字状态码,不建议直接使用数值,而是使用枚举。

// bad
var param = {
    periodType: 0,
};


// good
const HOUR = 0, DAY = 1;
var param = {
    periodType: HOUR,
};

2.6 避免使用成员前缀

把类和函数做得足够小,消除对成员前缀的需要。因为长期以后,前缀在人们眼里会变得越来越不重要。

2.7 添加有意义的语境

对于某些名称,在不同语境下可能代表不同的含义,最好为它添加有意义的语境。

firstName,lastName,street,houseNumber,city,state,zipcode 一连串变量放在一起可以判断是一个地址,但是如果将这些变量单独拎出来,有些变量名意义就不明确了。这时可以添加语境明确其意义,如 addrFirstName,addrLastName,addrState。

当然也不要随意添加语境,这样只会让变量名变得冗长。

// bad
var firsName, lastName, city, zipcode, state;
var sku = {
    skuName: 'sku0',
    skuStorage: 'storage0',
    skuCost: '10',
};


// good
var addrFirsName, addrLastName, city, zipcode, addrState;
var sku = {
    name: 'sku0',
    storage: 'storage0',
    cost: '10',
};

2.8 变量名从一而终

变量名取名多花一点时间,如果这一对象会在多个函数,模块中使用,就应该使用一致的变量名,否则每次看到这个对象,都需要重新去理清变量名,造成阅读障碍。

// bad
function searchGoods(searchText) {
    getList({
        keyword: searchText,
    });
}
function getList(option) {

}

// good
function searchGoods(keyword) {
    getList({
        keyword: keyword,
    });
}

function getList(keyword) {}

3 函数

3.1 短小

短小是函数的第一规则,过长的函数不仅会造成阅读困难,在维护的时候难度也会增加。短小,要求每个函数做尽可能少的事情,同时减少代码的嵌套和缩进,要知道,代码的嵌套和缩减同样会带来阅读的困难。

// bad
function initPage(initParams) {
    var data = this.data;
    if ('dimension' in initParams) {
        data.dimension = initParams.dimension;
        data.tab.source.some(function(item, index){
            if (item.value === data.dimension) {
                data.tab.defaultIndex = index;
            }
        });
    }
    if ('standardMedium' in initParams) {
        data.hasStandardMedium = true;
        data.filterParams[data.dimension].standardMedium = initParams.standardMedium;
    }
    if ('plan' in initParams || 'name' in initParams) {
        data.filterParams[data.dimension].planQueryString = initParams.plan || initParams.name;
    } else if ('traceId' in initParams) {
        data.filterParams[data.dimension].planQueryString = 'id:' + initParams.traceId;
    }
}

// good
function initPage(initParams) {
    initDimension(initParams);
    initStandardMedium(initParams);
    initPlanQueryString(initParams);
}
function initDimension(initParams) {
    var data = this.data;
    if ('dimension' in initParams) {
        data.dimension = initParams.dimension;
        data.tab.source.some(function(item, index){
            if (item.value === data.dimension) {
                data.tab.defaultIndex = index;
            }
        });
    }
}
function initStandardMedium(initParams) {
    var data = this.data;
    if ('standardMedium' in initParams) {
        data.hasStandardMedium = true;
        data.filterParams[data.dimension].standardMedium = initParams.standardMedium;
    }
}
function initPlanQueryString() {
    var data = this.data;
    if ('plan' in initParams || 'name' in initParams) {
        data.filterParams[data.dimension].planQueryString = initParams.plan || initParams.name;
    } else if ('traceId' in initParams) {
        data.filterParams[data.dimension].planQueryString = 'id:' + initParams.traceId;
    }
}

3.2 只做一件事情

函数应该做一件事情,做好这件事,只做这一件事。

如果函数只是做了该函数名下同一个抽象层上的步骤,则函数还是只做了一件事。当函数中出现另一抽象层级所做的事情时,则可以将这部分拆成另一层级的函数,因此缩小函数。

当一个函数可以被划分成多个区段时(代码块)时,这就说明了这个函数做了太多事情。

// bad
function onTimepickerChange(type, e) {
    if(type === 'base') {
        // do base type logic...
    } else if (type === 'compare') {
        // do compare type logic...
    }
    // do other stuff...
}

// good
function onBaseTimepickerChange(e) {
    // do base type logic
    this.doOtherStuff();
}

function onCompareTimepickerChange(e) {
    // do compare type logic
    this.doOtherStuff();
}

function doOtherStuff(){}

3.3 每个函数一个抽象层级

一个函数中不应该混杂了多个抽象层级,即同一级别的步骤才放到一个函数中,因为通过这些步骤就能完整地完成一件事情。

回到之前提到变量命名的问题,一个变量或函数,其作用域余越广,就越需要一个有意义的名字来对其进行描述,提高可读性,减少在阅读代码时还需要去查询定义代码的频率,有些时候有意义的名字就可能需要更多的字符,但这是值得的。但对于小范围使用的变量和函数,可以适当缩短名称。因为过长的名称,某些时候反而会增加阅读的困难。

可以通过向下原则划分抽象层级

程序就像是一系列 TO 起头的段落,每一段都描述当前层级,并引用位于下一抽象层级的后续 TO 起头段落
- 如果要完成 A,需要完成 B,完成 C;
- 要完成 B,需要完成 D;
- 要完成 C,需要完成 E;

函数名明确了其作用,获取一个图表和列表,函数中各个模块的逻辑进行了划分,明确各个函数的分工, 拆分的函数名直接表明了每个步骤的作用, 不需要额外的注释和划分。在维护的时候, 可以快速的定位各个步骤, 而不需要在一个长篇幅的函数中需找对应的代码逻辑.

实际业务例子, 数据门户-流量看板-流量总览的一个获取趋势图和右边列表的例子。选择一个通过 tab 选择不同的指标,不同的指标影响的趋势图和右边列表的内容,两个模块的数据合并到一个请求中得到。流水账的写法可以将函数写成下面的样子,这种写法有几个明显的缺点:

  • 长。通常情况下趋势图配置可能就需要20多行,整个函数加起来,轻易就超过50行了;
  • 函数名不准确。函数名仅表明是获取一个图表的,但实际上还获取了右边列表数据并进行了配置;
  • 函数层级混乱,还可以进行更细的划分;

根据向下原则

// bad
getChart: function(){
    var data = this.data;
    var option = {
        url: '/chartUrl',
        param: {
            dimension: data.dimension,
            period: data.period,
            comparePeriod: data.comparePeriod,
            periodType: data.periodType,
        },
        fn: function(json){
            var data = this.data;
            // 设置图表
            data.chart = json.data.chart;
            data.chart.config = {
                //... 大量的图表配置,可能有20多行
            }
            // 设置右边列表
            data.sideList = json.data.list;
        }
    };
    // 获取请求参数
    this.fetchData(option);
},

// good
getChartAndSideList: function(){
    var option = {
        url: '/chartUrl',
        param: this.getChartAndSideListParam();
        fn: function(json){
            this.setChart(json);
            this.setSideList(json);
        }
    };
    this.fetchData(option);
},

3.4 switch语句

switch语句会让代码变得很长,因为switch语句天生就是要做多件事情,当状态不断增加的时候,switch语句也会不断增加。因此可能把取代switch语句,或者将其放在较低的层级.

放在底层的意思,可以理解为将其埋藏到抽象工厂地下,利用抽象工厂返回内涵不同的方法或对象来进行处理.

3.5 减少函数的参数

函数的参数越多,不仅注释写得长,使用的时候容易使得函数参数发生错位。当函数参数过多时,可以考虑以参数列表或者对象的形式传入.

数据门户里面的一个例子:

// bad
function getSum(a [, b, c, d, e ...]){}


// good
function getSum(arr){}
// bad
function exportExcel(url, param, onsuccess, onerror){}


// good
/**
 * @param option
 *    @property url
 *    @property param
 *    @property onsucces
 *    @property onerror
 */
function exportExcel(option){}

参数尽量少,最好不要超过 3 个

3.6 取个好名字

函数应该取个好一点的名字,适当使用动词和关键字可以提高函数的可读性。例如:

一个判断是否在某个区间范围的函数,取名为 within,从名称上可以容易判断出函数的作用,但是这仍然不是最好的,因为这个函数带有三个参数,无法一眼看出这个函数三个参数之间的关系,是 b <= a && a<= c,还是 a <= b && b <= c ?

或许可以通过更改参数名来表达三个参数的关系,这个必须看到函数的定义后才可能得知函数的用法.

如果再把名字改一下,从名字就可以容易得知三个参数依次的关系,当然这个名字可能会很长,但如果这个函数需要大范围地使用,较长的名字换来更好的可读性,这一代价是值得的.

// bad
function within(a, b, c){}

// good
function assertWithin(val, min, max){}

// good
function assertValWithinMinAndMax(val, min, max){}

3.7 无副作用

一个有副作用的函数,通常都是是非纯函数,这意味着函数做的事情其实不止一件,函数所产生的副作用被隐藏了,函数调用者无法直接通过函数名来明确函数所做的事请.

4 注释

4.1 好注释

法律信息,提供信息的注释,对意图的解释,阐释,警示,TODO,放大(放大某种看似不合理代码的重要性),公共 API 注释

尽量让函数,变量变得刻度,不要依赖注释来描述,对于复杂难懂的部分才适当用注释说明.

4.2 坏注释

喃喃自语,多余的注释(例如本来函数名就能够说明意图,还要加注释),误导性注释,循规式注释(为了规范去加注释,其实函数名和参数名已经可以明确信息了),日志式注释(记录无用修改日志的注释),废话注释

4.3 原则

  1. 能用函数或变量说明时,就别用注释,这就意味着要花点时间取个好名字
// bad
var d = 10;     // 天数

// good
var days = 10;
  1. 注释掉的代码不要留,重要的代码是不会被注释掉的

数据门户-实时概况里面的一段代码,/src/javascript/realTimeOverview/components/index.js

// bad
function dimensionChanged(dimension){
    var data = this.data.keyDealComposition;
    data.selectedDimension = dimension;
    // 2016.10.31 modify:产品改动,选择品牌分布的时候不显示二级类目
    // if (dimension.dimensionId == '6') {
    //     data.columns[0][0].name = dimension.dimensionName;
    //     data.columns[0].splice(1, 0, {name:'二级类目', value:'secCategoryName', noSort: true});
    // } else {
        this.handle('util.setTableHeader');
    // }
    this.handle('refreshComposition');
};

// good
function dimensionChanged(dimension){
    var data = this.data.keyDealComposition;
    data.selectedDimension = dimension;
    this.handle('util.setTableHeader');
    this.handle('refreshComposition');
};
  1. 不要在注释里面加入太多信息,没人会看
  2. 非公用函数,没有必要加过多的注释说明,冗余的注释会使代码变得不够紧凑,增加阅读障碍
// bad
/**
 * 设置表格表头
 */
function setTableHeader(){},

// good
function setTableHeader(){},
  1. 括号后的注释
// bad
function doSomthing(){
    while(!buffer.isEmpty()) {  // while 1
        // ...
        while(arr.length > 0) {  // while 2
            // ...
            if() {

            }
        } // while 2
    } // while 1
}
  1. 不需要日志式,归属式注释,相信版本控制系统
// bad
/**
 * 2016.12.03 bugfix, by xxxx
 * 2016.11.01 new feature, by xxxx
 * 2016.09.12 new feature, by xxxx
 * ...
 */


// bad
/**
 * created by xxxx
 * modified by xxxx
 */
function addSum() {}

/**
 * created by xxxx
 */
function getAverage() {
    // modified by xxx
}
  1. 尽量别用用位置标记
// bad

/*************** Filters ****************/

///////////// Initiation /////////////////

5 格式

5.1 垂直方向

  1. 相关代码紧凑显示,不同部分的用空格隔开
// bad
function init(){
    this.data.chartView = this.$refs.chartView;
    this.$parent.$on('inject', function () {
        this.dataConvert(this.data.source);
        this.draw();
    });
    this.$watch('source', function (newValue, oldValue) {
        if (newValue && newValue != this.data.initValue) {
            this.dataConvert(newValue);
            this.draw();
        } else if (!newValue) {
            if (self.data.chartView) {
                this.data.chartView.innerHTML = '';
            }
        }
    }, true);
}

// good
function init(){
    this.data.chartView = this.$refs.chartView;

    this.$parent.$on('inject', function () {
        this.dataConvert(this.data.source);
        this.draw();
    });

    this.$watch('source', function (newValue, oldValue) {
        if (newValue && newValue != this.data.initValue) {
            this.dataConvert(newValue);
            this.draw();
        } else if (!newValue) {
            if (this.data.chartView) {
                this.data.chartView.innerHTML = '';
            }
        }
    }, true);
}
  1. 不要在代码中加入太多过长的注释,阻碍代码阅读
// bad
BaseComponent.extend({
    checkAll: function(status){
        status = !!status;
        var data = this.data;
        this.checkAllList(status);
        this.checkSigList(status);
        data.checked.list = [];
        if(status){
            // 当全选的时候先清空列表, 然后在利用Array.push添加选中项
            // 如果在全选的时候不能直接checked.list = dataList
            // 因为这样的话后面对checked.list的操作就相当于对dataList直接进行操作
            // 利用push可以解决这一个问题
            data.sigList.forEach(function(item,i){
                data.checked.list.push(item.data.item);
            })
        }
        this.$emit('check', {
            sender: this,
            index: CHECK_ALL,
            checked: status,
        });
    },
});

// good
BaseComponent.extend({
    checkAll: function(status){
        status = !!status;
        this.checkAllList(status);
        this.checkSigList(status);
        this.clearCheckedList();
        if(status){
            this.updateCheckedList();
        }

        this.emitCheckEvent(CHECK_ALL, status);
    },
});
  1. 函数按照依赖顺序布局,被调用函数应该紧跟调用函数
// bad
function updateModule() {}
function updateFilter() {}
function reset() {}
function refresh() {
    updateFilter();
    updateModule();
}

// good
function refresh() {
    updateFilter();
    updateModule();
}
function updateFilter() {}
function updateModule() {}
function reset() {}
  1. 相关的,相似的函数放在一起
// bad
function onSubmit() {}
function refresh() {}
function onFilterChange() {}
function reset() {}

// good
function onSubmit() {}
function onFilterChange() {}

function refresh() {}
function reset() {}
  1. 变量声明靠近其使用位置
// bad
function (x){
    var a = 10, b = 100;
    var c, d;

    a = (a-b) * x;
    b = (a-b) / x;
    c = a + b;
    d = c - x;
}

// good
function (x){
    var a = 10, b = 100;

    a = (a-b) * x;
    b = (a-b) / x;

    var c = a + b;
    var d = c - x;
}

5.2 水平方向

  1. 运算符号之间空格,但是要注意运算优先级
// bad
var v = a + (b + c) / d + e * f;

// good
var v = a + (b+c)/d + e*f;
  1. 变量水平对齐意义不大,应该让其靠近
// bad
var a       = 1;
var sku     = goodsInfo.sku;
var goodsId = goodsInfo.goodsId;

// good
var a = 1;
var sku = goodsInfo.sku;
var goodsId = goodsInfo.goodsId;

5.4 对于短小的if,while语句,也要尽量保持缩进

突然间改变缩进的规律,很容易就会被阅读习惯欺骗

// bad
if(empty){return;}


// good
if(empty){
    return;
}

// bad
while(cli.readCommand() != -1);
app.run();


// good
while(cli.readCommand() != -1)
;

app.run();

6 实际业务代码中的应用

庞大的config函数

对于一些较为复杂的组件或页面组件,需要定义很多属性,同时又要对这部分属性进行初始化和监听,像下面这段代码。在好几个大型的页面里面都看到了类似的代码,config 方法少的有 100行,多的有 400行。

config 方法基本就是一个组件的入口,在进行维护的时候一般都会先读 config 方法,但是对于这么长的函数,很容易第一眼就懵了。

Component.extend({
    template: tpl,
    config: function(data){
        eu.extend(data, {
            tabChartTab: 0,
            periodType: 0,
            dimensionType: 1,
            dealConstituteCompare:false,
            dealConstituteSort: {
                dimensionValue: 'sales',
                sortType: 0,
            },
            dealConstituteDecorate: {
                noCompare:[],
                progress: ['salesPercent'],
                sort:[
                ]
            },
            defaultMetrics: [
            ],
            // ...下面还有几百行关于其他模块的属性, flow, hotSellRank等
        });

        this.$watch('periodType', function(){
            // ...
        });

        this.$watch('topCategoryId', function(){
            // ...
        });

        // 这里还有一部分异步请求代码...
        this.refresh();
    },
})

针对上述这段代码代码,明显的缺点是:

  • 太长
  • 变量命名有冗余信息,且搜索性差
  • 变量(属性)太多
  • 做的事情太多,初始化组件属性,添加监听方法,还有一些业务逻辑代码

这对这些可以作出一些改进:

  • 使用枚举代替数值
  • config内只保留一切作为范围加大属性的直接初始化代码,其余针对于模块的属性将通过调用 initData 方法来初始化
  • initData 进一步根据模块划分初始化方法
  • 对于属于摸个模块的属性,则将其划分到同一个对象上,减少组件上挂载的属性数量,同时也简化了属性的命名
  • 监听方法同样是通过 addWatchers 初始化
  • 初始化过程中需要执行的部分逻辑,尽可能放在 init 等组件实例化后执行
const TAB_A = 0, TAB_B = 1;
const HOUR = 0, DAY = 1;
const DIMENSION_A = 0, DIMENSION_B = 1;
const DISABLE = false, ENABLE = true;

Component.extend({
    template: tpl,
    config: function(data){
        eu.extend(data, {
            tabChartTab: TAB_A,
            periodType: HOUR,
            dimensionType: DIMENSION_B,
        });

        this.initData();
        this.addWatchers();
    },

    initData: function(){
        this.initDealConsitiuteData();
        this.initFlowData();
        this.initHotSellRank();
    },

    initDealConsitiuteData: function(){
        this.data.dealConstitute = {
            compare: DISABLE,
            sort: {
                dimensionValue: 'sales',
                sortType: 0,
            },
            decorate: {
                noCompare:[],
                progress: ['salesPercent'],
                sort:[
                ]
            },
            defaultMetrics: [
            ],
        }
    },

    addWatchers: function(){
        this.$watch('periodType', function(){
            // ...
        });

        this.$watch('topCategoryId', function(){
            // ...
        });
    },

    init: function(){
        // 部分初始化要执行的逻辑
        this.refresh();
    },

})

其实按照上面进行优化以后,代码的可读性是有所提高,但由于这是一个页面组件,代码行数极多,修改后方法变得更多了,仍然不便于阅读。所以,针对于这种大型的页面,更适当的做法是,将页面拆分为几个模块,将业务逻辑拆分,减少每个模块的代码量,提高可读性。而对于不可再拆分的组件或模块,如果仍然包含大量需要初始化的属性,上述例子就可以作为参考了。

7 总结

本文整理的几个要点:

  • 写代码就像写故事,里面各个角色 (变量,函数) 的名字要取得好,才读得流畅;
  • 函数要短小,不要混杂太多不相关,不同层级的逻辑;
  • 注释要精简准确,能不写就不要写;
  • 代码布局要向报纸学习,排版注意垂直与水平方向的间隔,联系紧密的布局要紧凑;

就算是经验老道的大神,也很难一遍就能写出简洁的代码,所以要勤于对代码进行重构,边写代码边修改。代码只有在经过一遍一遍修改和锤炼以后,才会逐渐地变得简洁和精致。

8 参考

  1. Clean Code
  2. clean-code-javascript

作者:网易考拉海购
链接:https://juejin.im/post/59c85c936fb9a00a4746de94
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。