Quantcast
Channel: IT社区推荐资讯 - ITIndex.net
Viewing all 11804 articles
Browse latest View live

Redis 的性能幻想与残酷现实

$
0
0

2011 年,当初选择 Redis 作为主要的内存数据存储,主要吸引我的是它提供多样的基础数据结构可以很方便的实现业务需求。另一方面又比较担心它的性能是否足以支撑,毕竟当时 Redis 还属于比较新的开源产品。但 Redis 官网宣称其是提供多数据结构的高性能存储,我们对其还是抱有幻想的。

幻想

要了解 Redis 的性能,我们先看看官方的基准性能测试数据,心里有个底。

测试前提
  Redis version 2.4.2
  Using the TCP loopback
  Payload size = 256 bytes  

  测试结果
  SET: 198412.69/s
  GET: 198019.80/s

这个数据刚一看觉得有点超出预期了,不过看了测试前提是规避了网络开销的,Client 和 Server 全在本机。而真实的使用场景肯定是需要走网络的,而且使用的客户端库也是不同的。不过这个官方参考数据当时让我们对 Redis 的性能还是抱有很大的期待的。

另外官方文档中也提到,在局域网环境下只要传输的包不超过一个 MTU (以太网下大约 1500 bytes),那么对于 10、100、1000 bytes 不同包大小的处理吞吐能力实际结果差不多。关于吞吐量与数据大小的关系可见下面官方网站提供的示意图。

验证

基于我们真实的使用场景,我们搭建了性能验证环境,作了一个验证测试,如下(数据来自同事 @kusix 当年的测试报告,感谢)。

测试前提
  Redis version 2.4.1
  Jmeter version 2.4
  Network 1000Mb
  Payload size = 100 bytes  

  测试结果
  SET: 32643.4/s
  GET: 32478.8/s

在实验环境下得到的测试数据给人的感觉和官方差了蛮多,这里面因为有网络和客户端库综合的影响所以没有实际的横向比较意义。这个实验环境实测数据只对我们真实的生产环境具有指导参考作用。在实验环境的测试,单 Redis 实例运行稳定,单核 CPU 利用率在 70% ~ 80% 之间波动。除了测试 100 bytes 的包,还测了 1k、10k 和 100k 不同大小的包,如下图所示:

诚然,1k 基本是 Redis 性能的一个拐点,这一点从上图看趋势是和官方图的一致。

现实

基于实验室测试数据和实际业务量,现实中采用了 Redis 分片来承担更大的吞吐量。一个单一 Redis 分片一天的 ops 波动在 20k~30k 之间,单核 CPU 利用率在 40% ~ 80% 之间波动,如下图。

这与当初实验室环境的测试结果接近,而目前生产环境使用的 Redis 版本已升级到 2.8 了。如果业务量峰值继续增高,看起来单个 Redis 分片还有大约 20% 的余量就到单实例极限了。那么可行的办法就是继续增加分片的数量来分摊单个分片的压力,前提是能够很容易的增加分片而不影响业务系统。这才是使用 Redis 面临的真正残酷现实考验。

残酷

Redis 是个好东西,提供了很多好用的功能,而且大部分实现的都还既可靠又高效(主从复制除外)。所以一开始我们犯了一个天真的用法错误:把所有不同类型的数据都放在了一组 Redis 集群中。

  • 长生命周期的用户状态数据
  • 临时缓存数据
  • 后台统计用的流水数据

导致的问题就是当你想扩分片的时候,客户端 Hash 映射就变了,这是要迁移数据的。而所有数据放在一组 Redis 里,要把它们分开就麻烦了,每个 Redis 实例里面都是千万级的 key。

而另外一个问题是单个 Redis 的性能上限带来的瓶颈问题。由于 CPU 的单核频率都发展到了瓶颈,都在往多核发展,一个 PC Server 一般 24或32 核。但 Redis 的单线程设计机制只能利用一个核,导致单核 CPU 的最大处理能力就是 Redis 单实例处理能力的天花板了。举个具体的案例,新功能上线又有点不放心,于是做了个开关放在 Redis,所有应用可以很方便的共享。通过读取 Redis 中的开关 key 来判断是否启用某个功能,对每个请求做判断。这里的问题是什么?这个 key 只能放在一个实例上,而所有的流量进入都要去这个 Redis GET 一下,导致该分片实例压力山大。而它的极限在我们的环境上不过 4 万 OPS,这个天花板其实并不高。

总结

认识清楚了现实的残酷性,了解了你所在环境 Redis 的真实性能指标,区分清幻想和现实。我们才能真正考虑好如何合理的利用 Redis 的多功能特性,并有效规避的它的弱项,再给出一些 Redis 的使用建议:

  • 根据数据性质把 Redis 集群分类;我的经验是分三类:cache、buffer 和 db
    • cache : 临时缓存数据,加分片扩容容易,一般无持久化需要。
    • buffer: 用作缓冲区,平滑后端数据库的写操作,根据数据重要性可能有持久化需求。
    • db : 替代数据库的用法,有持久化需求。
  • 规避在单实例上放热点 key。
  • 同一系统下的不同子应用或服务使用的 Redis 也要隔离开

另外,有一种观点认为用作缓存 Memcache 更合适,这里可以独立分析下其中的优劣取舍吧。Memcache 是设计为多线程的,所以在多核机器上单实例对 CPU 的利用更有效,所以它的性能天花板也更高。(见下图)要达到同样的效果,对于一个 32 核机器,你可能需要部署 32 个 Redis 实例,对运维也是一种负担。

除此,Redis 还有个 10k 问题,当缓存数据大于 10k(用作静态页面的缓存,就可能超过这个大小)延迟会明显增加,这也是单线程机制带来的问题。如果你的应用业务量离 Redis 的性能天花板还比较远而且也没有 10k 需求,那么用 Redis 作缓存也是合理的,可以让应用减少多依赖一种外部技术栈。最后,搞清楚现阶段你的应用到底需要什么,是多样的数据结构和功能、更好的扩展能力还是更敏感的性能需求,然后再来选择合适的工具吧。别只看到个基准测试的性能数据,就欢呼雀跃起来了。


额外扯点其他的,Redis 的作者 @antirez 对自己的产品和技术那是相当自信。一有人批评 Redis 的问题,他都是要跳出来在自己的 blog 里加以回应和说明的。比如有人说 Redis 功能多容易使用但也容易误用,作者就跑出来解释我设计是针对每种不同场景的,你用的不对怪我咯,怪我咯。有人说缓存场景 Memcache 比 Redis 更合适,作者也专门写了篇文章来说明,大概就是 Memcache 有的 Redis 都有,它没有的我还有。当然最后也承认多线程是没有的,但正在思考为 Redis I/O 增加线程,每个 Client 一个线程独立处理,就像 Memcache 一样,已经等不及要去开发和测试了,封住所以批评者的嘴。

Redis 这些年不断的增加新功能和优化改进,让它变得更灵活场景适应性更多的同时,也让我们在使用时需要更细致的思考,不是它有什么我就用什么,而是你需要什么你就选择什么。

这篇先到这,后面还会再写写关于 Redis 扩展方面的主题。

参考

[1] antirez. Redis Documentation.
[2] antirez. Clarifications about Redis and Memcached.
[3] antirez. Lazy Redis is better Redis.
[4] antirez. On Redis, Memcached, Speed, Benchmarks and The Toilet.
[5] antirez. An update on the Memcached/Redis benchmark.
[6] dormando. Redis VS Memcached (slightly better bench).
[7] Mike Perham. Storing Data with Redis.
[8] 温柔一刀. Redis 常见的性能问题和解决方法.


我懂得所有的投资道理,却依然做不好股票

$
0
0
戴汨

聪明,其实不是一个好词。

说到聪明人,大部分的人脑子里第一印象都是智商高的人或者那些名校校毕业的人。聪明人在学校里面通常比较受尊重,他们也自然而然的享受这种尊重。

但是一个有趣的现象是,大部分的聪明人到了社会上就失去了优势。拿世俗的标准来看,那些社会上的成功人士大都不是学校里的好学生或者连有名的学校都不是。

马云说:中国最好的大学是杭州师范大学。这固然是调侃,但其实很有道理。

那么,聪明人为什么难以成功呢?搞清楚下面的4条规则或许对聪明人有帮助。

1. 最好的成功策略是:找到傻瓜

Peter Thiel在他的书《从0到1》里表达过类似的观点。最容易成功的领域不是聪明人和聪明人竞争,而是要找到那些和傻瓜竞争的地方。

那些从事律师、咨询师职业的人很多都是从哈佛、耶鲁、宾大等常青藤名校毕业的,但是他们都没有成为最富有的人,原因何在?简单的讲:一个人获得的价值不等于一个人的聪明程度。

一个人获得的价值=其聪明程度-周边人的聪明程度。

所以,你获得的价值是相对价值。当你选择高大上行业的时候,你的同类都是聪明的人。于是,你的价值基本上就被cancel out了。

广告行业里头有一个定律和这个道理类似:你投放的广告效果不直接和你投放广告的声音(银子)成正比,而是取决于你的声音比别人的声音大多少。

很多聪明人享受高智商竞争带来的快感,但是忘了高质量的竞争对聪明人其实是消耗战。

2. 回归平均原理(Return to mean)

统计上一个非常重要的定律是回归平均原理。

什么意思?打个比方,你是一个普通人,朝着墙上的一个靶心扔飞镖,如果第一次能够击中靶心,那么第二次肯定偏离很多。而如果第一次偏离的非常远,第二次偏离就会变小。

要清楚这一点,就要了解什么是运气。

运气在数学上可以看成是个随机的函数。

西方有个名言:success= some talent luck; great scucess= some talent great luck; 一件事只做一次,运气对成功而言会扮演重要角色。

但是当你做多次以后,运气的成分就会渐渐消失,因为运气是随机函数会抵消,技术(talent)变为主要的决定因素。

当你扔一次飞镖的时候,如果碰巧命中了靶心,运气扮演很重要的成分,但是当你多次扔的时候,就会回归到你的技术能力。

发展事业也是同样的道理,很多聪明的人,由于机会成本高,通常不太愿意坚持。

也就是说,他们通常不去尝试,或者尝试了一次就放弃,而由于只尝试一次,运气会起很大作用,他们的聪明天分不一定帮上忙。

但是反过来,如果他们坚持尝试,他们高于常人的聪明天赋就会起重要的作用。这就是我们经常讲的,冒险精神很重要。冒险的本质就是不断尝试。

3. 复利原理

巴菲特说:复利是最伟大的金融工具。复利的原理就是正向迭代。利息越高,时间越长,产生的结果就越惊人。这给我们的启示有三个:1)正向 2)进步步长要大 3)要长期坚持。

聪明人经常犯的毛病就是赢在起点,但也输在起点。他们通常比别人有一个好的开始,但是在他们的人生中忽视了持续的学习进步。

一个人的起点即使比普通人高10倍,只要对方的进步快,加以时日,就可以获得远远超过聪明人的成就。曾国藩的一句名言就是:聪明人要下死功夫。

4. 系统基准(base rate)

聪明的人经常把注意力投射在自己身上,强调自身的天赋,强调自身的努力,而经常忘了外部的系统和规律。

中国的古代就意识到这个问题,所有才有“天时地利人和”的说法。在这一点,西方人不如中国人。什么是系统基准?举个例子来讲,人人都希望获得长寿,但是长寿这件事放到一个超越个人的系统里来看,就是人的寿命。

人的寿命,几千年来就有一个比较明确的分布,基准是70,波动的范围一般而言(统计学规范讲可能说3个sigma)是(-20,20),也就是说大部分人会活到50~90岁。这个时候,个人的努力在这个范围内是有效地,但是如果非常努力希望活到150岁,就是违背了系统基准的事情。

创业和工作也有同样的情况。

毕业找工作有句老话:男怕入错行。创业也怕选错了行业。每个行业都有不同的基准,可以看成是这个行业的局限或者规律。如果不了解这个,只是一味强调团队的优秀和努力,结果很难好到那里去。

这个道理,其实也简单,你在沙滩上用手堆沙子,任你如何聪明勤奋,你也不可能堆出1米的高度来,你面对是沙子的粘性这个系统基准。

说了这么多,总结起来就是下面4条:
1)聪明人不要和聪明人竞争
2)要敢于多次冒险
3)要长期学习进步
4)要理解所处系统的规律

来源:创业邦 作者:戴汨
 

Uber容错设计与多机房容灾方案

$
0
0

此文是根据赵磊在【QCON高可用架构群】中的分享内容整理而成。

赵磊,Uber高级工程师,08年上海交通大学毕业,曾就职于微软,后加入Facebook主要负责Messenger的后端消息服务。这个系统在当时支持Facebook全球5亿人同时在线。目前在Uber负责消息系统的构建并推进核心服务在高可用性方向的发展。

前言

赵磊在7月21号的全球架构师峰会深圳站上,做了主题演讲:Uber高可用消息系统构建,对于这个热门主题,高可用架构群展开了热议,大家对分布式系统中的各种错误处理非常感兴趣。Tim Yang特邀赵磊通过微信群,在大洋彼岸的硅谷给大家进一步分享。

分布式系统单点故障怎么办

non-sharded, stateless 类型服务非常容易解决单点故障。 通常load balancer可以按照固定的时间间隔,去health check每个node, 当某一个node出现故障时,load balancer可以把故障的node从pool中排除。

很多服务的health check设计成简单的TCP connect, 或者用HTTP GET的方式,去ping一个特定的endpoint。当业务逻辑比较复杂时,可能业务endpoint故障,但是health endpoint还能正常返回,导致load balancer无法发现单点故障,这种情况可以考虑在health check endpoint中增加简单的业务逻辑判断。

对于短时间的network故障,可能会导致这段时间很多RPC call failures。 在RPC client端通常会实现backoff retry。 failure可能有几种原因:

  1. TCP connect fail,这种情况下retry不会影响业务逻辑,因为Handler还没有执行。

  2. receive timeout, client无法确定handler是不是已经收到了request 而且处理了request,如果handler重复执行会产生side effect,比如database write或者访问其他的service, client retry可能会影响业务逻辑。

对于sharded service,关键是如何找到故障点,而且将更新的membership同步到所有的nodes。下面讨论几种sharding的方案:

  1. 将key space hash到很多个小的shard space, 比如4K个shards。 通过zookeeper (distributed mutex) 选出一个master,来将shard分配到node上,而且health check每一个node。当遇到单点故障时,将已经assigned的shards转移到其他的nodes上。 因为全局只有一个single master, 从而保证了shard map的全局一致。当master故障时,其他的backup node会获得lock成为Master

  2. Consistent hashing方式。consistent hashing 通常用来实现cache cluster,不保证一致性。 因为每个client会独立health check每一个node, 同时更新局部的membership。 在network partition的情况或者某一个node不停的重启, 很可能不同的client上的membership不一致,从而将相同的key写在了不同的node上。 当一致性的需求提高时,需要collaborative health check, 即每个node要monitor所有其他node的health。 Uber在这里使用的是gossip protocol,node之间交换health check的信息。

大面积故障怎么办

大面积故障时,比如交换机故障(rack switch failure),可用的机器不足以处理所有的请求。 我们尽可能做的就是用50%的capacity 处理50%的请求或者50%用户的所有请求。而尽量避免整个服务故障。 当设计一个服务的时候,它的throughput应该是可linear scale的。

  1. 在同样的CPU占用情况下,1个机器应该处理100个请求,那么5个机器应该可以处理500个请求。

  2. 而且在同样的机器数量下,20%的CPU可以处理200个请求,那么60%的CPU应该可以处理3倍即600个请求。

后者是很难实现的,而且当CPU越高的时候,服务的throughput并不是线性的。 通常在80%CPU以上的情况,throughput会下降非常快。 随着CPU使用增加,request的latency也会提高。 这对上下游的服务可能都是一个挑战,可能会导致cascade failure。

对于nodejs或者java nio一类的async IO框架来说,另外一个问题就是event loop lag。 这两者可能导致connection数量增加。下面举两个例子

  1. 有些RPC transport支持pipelining但不支持multiplexing (out of order responses), pipelining是指在同一个TCP连接上可以连续发出Req1, Req2, Req3, Response1, Response2, Response3,即Response的顺序必须和Request的顺序是一致。Req1如果需要很长时间,Req2和3就都不能返回。一个Request如果占用太长时间,会导致后面的很多个Request timeout。RPC client通常也会限制在一个TCP connection上面的max pending requests。但timeout发生,或者max pending requests情况下,client会主动创建新的connection。

  2. event loop lag 是指程序占用太长时间执行连续的CPU intensive任务。 只有当任务结束时,event loop才会handle IO events,比如从socket上面读数据。否则收到的数据只能保存在kernel 的TCP buffer里,通常这个buffer size小于64KB。当buffer满时(而且service又很长时间没有读buffer),socket的远端就不能发送更多的数据。这时也会导致远端的transport error。同样的,client会主动创建新的connection,当connection增加到预设的fd limit时,service就不能继续accept新的TCP connection了,其实是不能open新的文件了。而且,绝大部分的程序没有测试过达到fd limit的场景。很多API需要open file, 比如logging和core dump. 所以,一旦达到fd limit, 就像out of memory一样,将很难recover,只能crash process. 而这时正是过载的时候,重启实际上减少了capacity。 任何crash在过载的情况下只会更糟。facebook在这防止过载上做的很好,在C++实现的thrift server上,有一个或者多个threads只负责accept TCP connections. 你可以指定最多的connections for thrift calls。 这个connection limit是远小于fd limit, 当connection太多时,thrift server可以fail fast。所以,这种情况下可以让service能一直保持在max qps。

整个数据中心挂掉怎么办

在Uber的场景中,如果rider已经在一个trip上了,我们通产会等trip结束后才把rider迁移到其他的数据中心,我们叫做soft failover。否则需要hard failover,我们会把DNS指向其他的数据中心。 而且用户的DNS服务器很可能在一段时间内还是cache以前的ip,而且这个cache的时间是基本没办法控制的,所以我们会在load balancer上返回HTTP redirect,这样手机的客户端收到后会立即转向新的备份数据中心。

惊群问题(thundering herd), 很多服务在provision的时候根据平常的QPS预留了很少的容量空间,当数据中心或者load balancer重启的时候,如果所有的客户端同时发起请求,这时的QPS可以是平时的很多倍。 很可能导致大部分请求都失败。一方面需要在客户端实现exponential backoff, 即请求失败后retry的间隔时间是增长的,比如1秒,5秒,20秒等等。另外在load balancer上实现rate limiting或者global blackhole switch, 后者可以有效的丢掉一部分请求而避免过载,同时尽早触发客户端的backoff逻辑。

如果大家用AWS或者其他云服务的话,AWS的一个region通常包括几个数据中心。各个数据中心甚至在相邻的介个城市,有独立的空调系统和供电。

数据中心之间有独立的网络 high throughput low latency, 但是在region之间的网络通常是共有的 high throughput high lantecy

整个region挂掉很少发生。可以把服务部署在多个可用区(Availability Zone)来保证高可用性。

Q & A

Q1:health check endpoint中实现简单的业务逻辑,这个意思是load balancer中有业务逻辑检查的插件么?这样load balancer会不会很重啊,可以详细说一下么?

load balancer仍然是HTTP GET, health check 没有额外的开销,但是服务本身处理health的方式不同,可加入业务逻辑相关的检查 比如是不是能够访问数据库。

Q2:region切换时,用户的数据是怎么迁移的?

这个是个很好的问题,Uber采取的是个非常特别的方法。 realtime系统会在每次用户state change。state change的时候把新的state下载到手机上,而且是加密的。当用户需要迁移到新的数据中心的时候,手机需要上传之前下载的state,服务就可以从之前的state开始,但是non-realtime系统 比如用户数据是通过sql replication来同步的。是Master-master。而且Uber在上层有个数据抽象,数据是基本上immutable的 append-only 所以基本不存在冲突。

Q3:如果是req timeout,但另外一边已经执行成功了,这时候重试,那不就是产生了两次数据?特别是insert这种类型的。

是的,如果是GET类型的请求可以retry, 但是POST类型的请求 那么只能在conn timeout时可以安全的retry。 但是receive timeout不能重试。(Tim补充看法:对于POST请求,如果service实现了幂等操作也是可以retry)。 有些类型的数据可以自动merge比如set和map

Q4:那receive timeout,这种情况下,只能通过merge或者冲突对比解决?

恩 是的。 需要在逻辑层判断是不是能够retry。 这个我建议在更上层实现, 比如在消息系统中,全程不retry 就可以保证at most once delivery, 如果需要保证at least once delivery 需要加入数据库和client dedupe

Q5:大面积故障时Uber用什么手段来控制只处理部分用户请求?

我们实现了一些rate limiting 和 circuit breaking的库,但是这时针对所有请求的。 我们现在还没有做到只处理某些用户的请求。

Q6:“将key space hash到相对小的shard space, 因为全局只有一个single master, 从而保证了shard map的全局一致” 这个方案每次计算shard node的时候,必须先询问下master么?

是的。 在client端有一个shard map的cache, 每隔几秒钟可以refresh, 如果是复杂的实现,则可以是master 推送shardmap change。

Q7:多个机房的数据是sharding存储(就是每个机房只存储一部分用户数据),还是所有机房都有所有用户全量数据?

Uber现在的做法是每个机房有所有用户的数据。 facebook的做法是一个机房有一部分用户的数据。

Q10:Uber的消息系统是基于nodejs的吗?客户端长链接的性能和效率方面如何优化?

是基于nodejs的。我们没有特别优化性能,不过stress test看起来2个物理机可以保持800K连接

Q11:Uber消息系统协议自己DIY吗? 是否基于TLS? PUSH消息QPS能达到多少?

是的,基于HTTPS。 具体QPS我不太记得了。

Q12:riak的性能如何?主要存储哪些类型的数据呢?存储引擎用什么?raik的二级索引有没有用到呢?

riak性能我没测试过,跟数据类型和consistency level都有关系。 可能差别比较大。 我们现在用的好像是leveldb

Q13:应用层实现多机房数据一致的话,是同时多写吗? 这个latency会不会太长?

sql现在都是用在non-realtime系统里面,所以latency可能会比较长

Q14:Uber rpc用的什么框架,上面提到了Thrift有好的fail fast策略,Uber有没有在rpc框架层面进行fail fast设计?

Uber在RPC方面还刚开始。 我们一直是用http+json的,最近在朝tchannel+thrift发展, tchannel是一个类似http2.0的transport,tchannel 在github上能找到。我们的nodejs thrift 是自己实现的,因为apache thrift在node上做的不是很好,thrift的实现叫做thriftify https://github.com/Uber/thriftify正好推荐下我的开源项目哈。 在thrift server上我们没有做fail fast, 如何保护是在routing service中实现的。

Q15:Uber走https协议,有没有考虑spdy/http2.0之类的呢?在中国网速状况不是很好的,Uber有没有一些https连接方面的优化措施?

正在考虑迁移到HTTP2.0,这个主要是手机端有没有相应的client实现。 server端我们用的是nginx,nginx上有个experiemnt quality的extension可以支持spdy。 我们还考虑过用facebook的proxygen https://github.com/facebook/proxygen,proxygen支持spdy。 我在facebook的chat service是用proxygen实现的,而且facebook 几十万台PHP server都在proxygen上,所以可以说是工业级强度的基础设施,不过build起来要花点时间。

Q16:为了避免服务过载和cascade failure,除了在服务链的前端采用一些fail fast 的设计,还有没有其它的实践作法,比如还是想支持一部分用户或特定类型的请求,采用优先级队列等。 就这个问题,Uber,facebook在服务化系统中还有没有其它技术实践?另外出现大规模服务过载后的恢复流程方面,有没有碰到什么坑或建议?

“比如还是想支持一部分用户或特定类型的请求” 这个其实比较难实现 因为当服务过载的时候 在acceptor thread就停止接受新的connection了,那就不知道是哪个用户的请求 。这个需要在应】用层实现,比如feature flag可以针对一些用户关掉一些feature。 我发现有个很有用的东西就是facebook有个global kill switch,可以允许x%的流量,这个当所有service一起crash 重启的时候比较有用。

 

此文是根据赵磊在【QCON高可用架构群】中的分享内容整理而成。

 



已有 0人发表留言,猛击->> 这里<<-参与讨论


ITeye推荐



使用动态代理实现精简版CachedRowSetImpl

$
0
0
曾经,为了避免“Access restriction” ,打算自动实现一个CachedRowSet  ,于是新建一个类implements CachedRowSet , 没有做其它任何工作,代码已经2千多行了, class文件38K !!!
所以多次因此放弃了。

今天想到用动态代理实现CachedRowSet ,于是只实现其中部分有用的方法,剩余的300多个无用方法不处理。几百行代码就解决问题 ,并且
   支持修改结果集内容,增加结果集的列,
   getXXX( ) 不会字字段不存在出现讨厌的SQLException
   不会因字段使用了别名出现字段不存在的问题

调用方式

ResultSet rs = statement.executeQuery(sql);
CachedRowSet rowSet = SimpleCachedRowSetImpl.newInstance();
rowSet.populate( rs );

ProxyHandler
public    class ProxyHandler implements InvocationHandler {
	private Object concreteClass;

	public ProxyHandler(Object concreteClass) {
		this.concreteClass = concreteClass;
	}

	public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { 
		try{
			method = concreteClass.getClass().getMethod(method.getName(), method.getParameterTypes() );
			Object object = method.invoke(concreteClass, args);// 普通的Java反射代码,通过反射执行某个类的某方法
			return object;
		}catch(NoSuchMethodException ex){
			if( method.getName().startsWith("set")){
				throw new NoSuchMethodException( ex.getMessage() +", 请使用setObject");
			}
			throw ex;
		}catch( InvocationTargetException ex){
			//抛出原始的错误避免过多的stacktrace
			throw ex.getCause();
		}
	}

}


SimpleCachedRowSetImpl
public class SimpleCachedRowSetImpl implements SimpleRowSet  {
	LinkedList<Object[]> data = new LinkedList<Object[]>();
	TreeMap<String,Integer > fieldNameMap = new TreeMap<String,Integer >( String.CASE_INSENSITIVE_ORDER );
	private static final int FETCHCOUNT = 2000; 
	private int cursor =-1;
	private int rowCount =0;
	private int pageSize = 0;
	private int columnCount =0;
	private ResultSetMetaData meta; 
	private SimpleCachedRowSetImpl(){}
	/**
	 * 创建一个 CachedRowSet 实例
	 * @return
	 */
	public static CachedRowSet newInstance() {
		SimpleCachedRowSetImpl rowset = new SimpleCachedRowSetImpl() ;
		InvocationHandler ih = new ProxyHandler( rowset );
		Class<?>[] interfaces = {CachedRowSet.class,SimpleRowSet.class};
		Object newProxyInstance = Proxy.newProxyInstance( SimpleCachedRowSetImpl.class.getClassLoader()  , interfaces, ih);
		return (CachedRowSet)newProxyInstance;
	}
	private void updateMeta(ResultSet rs) throws SQLException{
		ResultSetMetaData oldmeta = rs.getMetaData();
		ResultSetMeta meta = new ResultSetMeta(oldmeta);
		columnCount = oldmeta.getColumnCount();
		for(int i=1;i<=columnCount;i++){
			Field field = new Field();
			field.catalogName = oldmeta.getCatalogName( i );
			field.columnClassName = oldmeta.getColumnClassName( i );
			field.columnType = oldmeta.getColumnType( i );
			field.columnLabel = oldmeta.getColumnLabel( i );
			field.columnName = oldmeta.getColumnName( i );
			field.precision  = oldmeta.getPrecision( i );
			field.scale  = oldmeta.getScale( i );
			field.schemaName = oldmeta.getSchemaName( i );
			field.table = oldmeta.getTableName( i );
			meta.addField(field);
		}
		this.meta = meta;
	}
	public void populate(ResultSet rs, int start) throws SQLException {
		if( start>-1){
			rs.absolute(start);
			rs.setFetchSize( this.pageSize );
		}else{

			rs.setFetchSize( FETCHCOUNT );
		}
		updateMeta(rs);
		rowCount = 0;
		while(rs.next()){
			Object[] row = new Object[ columnCount ];
			for(int i=1;i<=columnCount ;i++ ){
				row[ i-1 ] = rs.getObject( i );
			}
			data.add( row );
			rowCount++;
			if(start>-1 &&  rowCount> pageSize)
				break;
		} 
		//设置字段序号
		for(int i=1;i<=columnCount;i++){
			String fieldName = meta.getColumnLabel( i ); //使用 as 中的名称
			fieldNameMap.put( fieldName , i -1  ); //从0开始 
		}
		rowCount = data.size();

	}

	public void populate(ResultSet rs ) throws SQLException {
		populate( rs , -1 ); 
	} 
	@Override
	public boolean next() throws SQLException{
		if( cursor<rowCount-1){
			cursor++;
			return true;
		}
		cursor = rowCount;
		return false;
	}
	@Override
	public void beforeFirst() throws SQLException{
		cursor=-1;
	}
	@Override
	public String getString(String key ) throws SQLException{
		Object value = getObject( key );
		if( value==null)
			return null;
		return value.toString();
	}
	@Override
	public String getString(int index ) throws SQLException{
		Object value = getObject( index );
		if( value==null)
			return null;
		return value.toString();
	}
	@Override
	public Object getObject(String key ) throws SQLException{
		int index = getIndex(key); 
		return getObject(index );
	}
	@Override
	public Object getObject(int index ) throws SQLException{
		if( index==-1)
			return null;
		if( index<1 || index> columnCount ){
			throw new SQLException("必须是1-"+columnCount + "之间的数字" );
		} 
		Object[] row = data.get( cursor );
		Object value = row[ index -1  ];
		return value;
	}

	@Override
	public int getInt(int index) throws SQLException {
		Object value = getObject(index);
		if(value==null)
			return 0 ;
		if( value instanceof Number ){
			return ((Number)value).intValue();
		}
		if( value instanceof BigDecimal ){
			return ((BigDecimal)value).intValue();
		}
		String svalue = value.toString();
		try{
			return Integer.parseInt( svalue );
		}catch(NumberFormatException ex){
			throw new SQLException(svalue+"无法转换为int类型");
		}
	}

	@Override
	public int getInt(String key) throws SQLException {
		int index = getIndex(key); 
		return getInt( index );
		
	}

	@Override
	public long getLong(int index ) throws SQLException {
		Object value = getObject(index);
		if(value==null)
			return 0 ;
		if( value instanceof Number ){
			return ((Number)value).longValue();
		}
		if( value instanceof BigDecimal ){
			return ((BigDecimal)value).longValue();
		}
		String svalue = value.toString();
		try{
			return Long.parseLong( svalue );
		}catch(NumberFormatException ex){
			throw new SQLException(svalue+"无法转换为long类型");
		}
	}

	@Override
	public long getLong(String key) throws SQLException {
		int index = getIndex( key ); 
		return getLong( index );
		
	}
	
	

	@Override
	public float getFloat(String key) throws SQLException {
		int index = getIndex( key ); 
		return getFloat( index );
	}

	@Override
	public float getFloat(int index) throws SQLException {
		Object value = getObject(index);
		if(value==null)
			return 0 ;
		if( value instanceof Number ){
			return ((Number)value).floatValue();
		}
		if( value instanceof BigDecimal ){
			return ((BigDecimal)value).floatValue();
		}
		String svalue = value.toString();
		try{
			return Float.parseFloat( svalue );
		}catch(NumberFormatException ex){
			throw new SQLException(svalue+"无法转换为float类型");
		}
	}

	@Override
	public BigDecimal getBigDecimal(int index) throws SQLException {
		Object value = getObject(index);
		if(value==null)
			return null ;
		return ((BigDecimal)value) ;
		
	}

	@Override
	public BigDecimal getBigDecimal(String key) throws SQLException {
		int index = getIndex( key ); 
		return getBigDecimal( index );
		
	}

	@Override
	public double getDouble(int index) throws SQLException {
		Object value = getObject(index);
		if(value==null)
			return 0 ;
		if( value instanceof Number ){
			return ((Number)value).doubleValue();
		}
		if( value instanceof BigDecimal ){
			return ((BigDecimal)value).doubleValue();
		}
		String svalue = value.toString();
		try{
			return Double.parseDouble( svalue );
		}catch(NumberFormatException ex){
			throw new SQLException(svalue+"无法转换为double类型");
		}
	}

	@Override
	public double getDouble(String key) throws SQLException {
		int index = getIndex( key ); 
		return getDouble( index );
		
	}
//	@Override
//	public Date getDate(String key) throws SQLException {
//		int index = getIndex( key ); 
//		return getDate( index );
//		
//	}
	private int getIndex(String key){
		Integer index = fieldNameMap.get(key); 
		if( index==null)
			return  -1;
		return index +1;
	}
//	@Override
//	public Date getDate(int index) throws SQLException {
//
//		Object value = getObject(index );
//		if(value==null)
//			return null ;
//		if( value instanceof Date ){
//			return ((Date)value);
//		}
//		throw new NotImplementedException();
//		
//	}
//
//	@Override
//	public Date getDate(int index, Calendar calendar) throws SQLException {
//		throw new NotImplementedException();
//		
//	}
//
//	@Override
//	public Date getDate(String fieldName, Calendar calendar) throws SQLException {
//		throw new NotImplementedException();
//		
//	}
//
//	@Override
//	public Time getTime(int index) throws SQLException {
//		throw new NotImplementedException();
//		
//	}
//
//	@Override
//	public Time getTime(String fieldName) throws SQLException {
//		throw new NotImplementedException();
//		
//	}
//
//	@Override
//	public Time getTime(int index, Calendar calendar) throws SQLException {
//		throw new NotImplementedException();
//		
//	}
//
//	@Override
//	public Time getTime(String fieldName, Calendar calendar) throws SQLException {
//		throw new NotImplementedException();
//		
//	}

	@Override
	public Timestamp getTimestamp(int index) throws SQLException {

		Object value = getObject(index );
		int type = meta.getColumnType( index );
		if( type== Types.TIMESTAMP ){
			return ((Timestamp)value);
		}
		throw new NotImplementedException();
		
	}

	@Override
	public Timestamp getTimestamp(String fieldName) throws SQLException {
		return getTimestamp( getIndex( fieldName ));
		
	}

//	@Override
//	public Timestamp getTimestamp(int index, Calendar calendar) throws SQLException {
//
//		throw new NotImplementedException();
//		
//	}
//
//	@Override
//	public Timestamp getTimestamp(String fieldName, Calendar calendar) throws SQLException {
//		throw new NotImplementedException();
//		
//	}
//	@Override
//	public Blob getBlob(String fieldName) throws SQLException {
//		throw new NotImplementedException();
//		
//	}

	@Override
	public boolean getBoolean(int index) throws SQLException {
		Object val = getObject(index);
		return new Integer(1).equals(val);
		
	}

	@Override
	public boolean getBoolean(String key) throws SQLException {
		Object val = getObject( key );
		return new Integer(1).equals(val);
		
	}

	@Override
	public byte getByte(int index) throws SQLException {
		Object val = getObject( index );
		if( val==null)
			return 0;
		return (Byte)val;
		
	}

	@Override
	public byte getByte(String key) throws SQLException {
		Object val = getObject( key );
		if( val==null)
			return 0;
		return (Byte)val;
		
	}

//	@Override
//	public byte[] getBytes(int index) throws SQLException {
//		Object val = getObject( index );
//		if( val==null)
//			return null;
//		return (byte[])val;
//		
//	}
//
//	@Override
//	public byte[] getBytes(String fieldName) throws SQLException {
//		return getBytes( getIndex(fieldName));
//		
//	}
	
	public void setPageSize( int pageSize ) throws SQLException{
		this.pageSize = pageSize;
	}
 

	@Override
	public ResultSetMetaData getMetaData() throws SQLException {
		return meta;
		
	}
	@Override
	public void setObject(int colIndex, Object value) throws SQLException  {
		if( colIndex<1){
			throw new SQLException("应从1开始");
		}
		Object[] row = getCurrentRow();
		if( colIndex>row.length){
			row =Arrays.copyOf( row ,  colIndex );
			data.add( cursor , row );
		}
		row[ colIndex -1 ] = value;
	} 
	private Object[] getCurrentRow() { 
		return data.get( cursor );
	}
	@Override
	public void setObject(String field, Object value) throws SQLException  {
		int index = getIndex( field );
		if( index==-1){
			addColumn( field );
			index = getIndex( field );
		}
		//Object[] row = getCurrentRow();
		setObject( index , value );
	}
	private void addColumn(String field) {
		fieldNameMap.put( field , fieldNameMap.size() );
		columnCount = fieldNameMap.size();
	}


}


余下程序见附件



已有 0人发表留言,猛击->> 这里<<-参与讨论


ITeye推荐



同样是产品经理,大公司和小公司究竟有什么区别?

$
0
0

【编者按】本文作者刘飞,前锤子科技产品经理。

同样是产品经理,大公司和小公司究竟有什么区别?

很多人会关心,大公司和小公司的产品经理究竟有什么区别。

我的体验,异同点有以下这些。

大小公司的产品经理区别在哪里?

  • 在工作流程方面:大公司讲规矩,小公司看效果。

大公司是讲规矩的,流程出了纰漏,问题很严重,整个机器就运转不起来。

举个例子,同样是淘宝,你是做商品展示的,却偏要对支付环节指手画脚;再或者你是做用户研究的,却偏把交互也帮忙做出来了。萝卜不在自己的坑里,结果就是会乱套。

人越多的团队、部门越多的公司,越容易因为这个扯皮。

而小公司更看重效果,你愿意多做些,那就多做些。惹人不高兴了,我开掉不高兴的人就是,你把事赶快做好最重要。

我见过很多技术强行参与产品设计,很多产品强行参与运营决策,甚至有前端产品经理兼做 HRD 的团队。只要这事儿做出来了、有效果了,那就是大家认可的。

虽然这么操作不可持续,但资源紧缺、时间紧张的情况下,效果一定是最优先的。这也是小团队动作快的原因,同时也是小团队问题暴露得多的原因。就跟用电池一样,用得久有用得久的办法,用得狠有用得狠的办法。

  • 用人:大公司看经验、资历,小公司更看重聪明、执行力

大公司的用人会考虑到很多因素,某个部门的领导一定程度上会有公关或者给部门做品牌的作用(『我们部门老大是 8 年工作经验/搜狗输入法创始人』),除了本身能在产品经理方面创造的价值,还要考虑是不是能很好地成长、适配未来的岗位,在这个公司有健康的工作状态,甚至对公司够不够忠心。

小公司则会为了尽快得到收益,只看重这个产品经理立刻能产出什么。是不是能画图?你上!是不是可以写文档?你上!是不是可以跟项目?你上!这时不会太在意刚才提到的那些,也不在意具体的方式合不合理,流程正确不正确。

所以这里多说一句,小公司的成长其实建立在近饱和的工作状态下,同时也建立在做的事情很杂、很乱、有时还很匪夷所思的基础上。对于内心强大并有充足学习能力的人来说,这是极好的成长途径,但 对于尚未有概念的产品新人来说,每天专注在工作量繁重的细枝末节上,未必有精力很好地总结、很好地成长

  • 策略方面,大公司老板定,小公司产品经理是有机会参与的。

所谓的点子或者主意,肯定都是由老板来出。策略级别的事情是永远不会交给普通产品经理的,至少也需要是产品合伙人。

小公司里,发声会更容易,更能够『上达天命』,也就更有在老板面前表现、被老板器重的机会。大不了有想法了在公司门口堵老板。你到阿里堵一个马云试试?


  • 大公司的产品经理是被塑造的,而小公司是可以自塑的。

换句话可以说成是: 大公司的产品经理必须要填已经挖好的坑,而小公司的产品经理,要自己挖想填的坑。

大公司里,岗位编制都特别清晰细致,你作为某个角色进去,那很可能就会一直在这个岗位上做下去了,即便是部门换岗,肯定也是雷同的角色。而小公司的优势是,有试错的机会,感觉目前的定位不合适,完全可以试着做点别的事情。

比如我见到很多朋友在创业期间发现了自己最合理的定位,之后的事业就一帆风顺了,但在大公司这样试错的成本就成倍地增加,公司才不管怎么帮你找到自己定位,公司只关心你现在能不能值你的工资。

同样是产品经理,大公司和小公司究竟有什么区别?

大小公司的产品经理又有什么相同的地方?

  • 产品设计流程,都会存在不合理的地方

大公司出现不合理的状况,原因一般是中间信息传达步骤过于冗长、沟通繁琐、各方推卸责任,尤其在产品设计上,如果是通过一些文档、格式化的信息来传递想法,必然会有错漏,不如一个人能从头到尾跟进下去。

小公司的产品设计出现不合理,就是老生常谈的问题了:拍脑门太多。小公司的老板会比较闲,有精力参与细节,经常会爱想(能表现自己聪明才智)的点子。同时,因为用户研究这块的缺失,产品经理也没有能力和资源来做更多验证,只能硬着头皮做了。

减少抱怨。糟心事总是会遇到的,好的产品经理在哪都能游刃有余,而糟糕的产品经理只会吐槽自己生不逢时。

  • 不管大公司还是小公司,都会接触到很多杂七杂八的事。

在接触到大公司之前,我以为只有小团队的产品经理才需要三头六臂、会三十六变。后来发现就产品经理这个岗位的特殊性而言,真的是什么都得会做。

大公司里流程健全、机制复杂后,很多事情都得需要去推进。有时候给别人讲方案得做好几天 PPT 请设计师同事帮忙做图,资源调配不平衡得去别的部门求帮助,甚至运营团队不给力也得自己下地发传单......这些倒真的不分公司大小。

大公司的环境相对有优势,因为杂七杂八的事情里面,几乎都是跟产品强相关的。小公司的甚至会有搬桌子、买器材这样的事,有自己去搞招聘、组织团队活动等...在大公司,至少行政、人力和后勤的工作是不用操心了。

  • 大公司和小公司做产品是不是靠谱,在于是不是存在很有经验的人。

很多人会犹豫自己选大公司还是小公司。有人说大公司制度全、做事专业,有人说小公司成长快、见识广,有人说大公司牛人多、学习机会多,有人说小公司接触面大、参与感强......我倒觉得判断时没必要想太多,就看一件事:这个地方牛人多不多、你能不能跟他学。

你认可的牛人,也会是你想成为的榜样,那跟他学习,不就是往你期待的方向发展。

过去专业的、牛逼的产品经理都是在大公司,这几年已经逐渐开始外流。有的小团队虽然资源不全,但有专业的人在,做事的方法至少能保证是用正经的方法来做的,不是瞎搞的。

最怕的就是,你原本是到一个团队锻炼的,但老板也不懂产品,让你自己 hold 住,美其名曰更好的成长机会,但其实每一步都没人懂该怎么做,然后每一步都是大家扯皮和撕逼,结局无非也就是老板说得算,你就成了炮灰。

随便列举了几点,希望能提供参考。

Office在线预览及PDF在线预览的实现方式大集合

$
0
0

一、服务器先转换为PDF,再转换为SWF,最后通过网页加载Flash预览

微软方:利用Office2007以上版本的一个PDF插件SaveAsPDFandXPS.exe可以导出PDF文件,然后再利用免费的swftools.exe工具生成swf格式的Flash文件,网页中加载flexpaper免费开源工具(有广告)实现Flash文件的预览。
优点:
1、有效的保护的源文件及文件的复制,不可复制也是缺点。
2、源码是自己的,版权有保证。
缺点:
1、服务器上必须安装Office软件。
2、导出PDF文件本身是个打印过程,Excel页面格式未设置,会出现一张表格打印出多页来,阅读体验大大下降。
3、转换过程非常耗费资源,低配的CPU几乎能跑满,服务器卡死。转换时间也非常漫长,这个时间主要是卡在了转换PDF上面。
4、转换完成服务器会遗留大量Excel、Word进程无法正常退出,有一些折中的解决办法,可以在网上搜索。
5、设置非常麻烦,本身微软官方的说法Office软件是客户端程序,在与IIS交互的时候本身就未设计。所以很多程序员把精力浪费在了调试程序上面。有两点在调试的时候需要注意。一个是在web.config中设置 <identity impersonate="true" userName="administrator" password="你的服务器管理员密码" />,一个是在Office软件的设置中设置跟桌面交互。
6、严重浪费磁盘空间,一个文件还需要一个PDF文件、一个SWF文件,是否每次都转换,纠结是要硬盘空间呢还是要CPU的资源。
参考链接:
http://www.cnblogs.com/expectszc/archive/2012/04/04/2432149.html 
http://www.cnblogs.com/liuning8023/archive/2013/03/04/2943482.html 
http://www.cxyclub.cn/n/29549/ 

非微软方:没有微软的Office软件可安装,只能用第三方的openoffice(开源、免费)来转换PDF文件,其它方面都一样,优缺点一样
参考链接:
http://blog.csdn.net/z69183787/article/details/17468039 


二、Office文档直接转换为SWF,通过网页加载Flash预览

利用flashpaper直接转换为SWF文件(虚拟打印机),然后利用flexpaper预览Flash文件。
flashpaper是Macromedia的一款产品,随着被Adobe公司收购,Macromedia对于这款软件早就放弃了,国内尚无人在程序中调试成功过。
参考链接:
http://www.dzwebs.net/1149.html 


三、office转Html、pdf转图片在线预览文件Html文件

利用DCOM配置直接操作Office文件,读取文件内容,导出Html文件
优点:
实践证明此方法不科学。
缺点:
1、服务器上必须安装Office软件。
2、配置麻烦,正如微软所说,读取Office不是这么干的。
3、转换的文件格式均丢失。
4、仅限于IIS服务器,利用ASP.net(C#)。
参考链接:
http://www.cnblogs.com/tangbinblog/archive/2012/11/29/2794110.html 


四、第三方ActiveX浏览器控件

如科瀚的SOAOffice中间件、卓正软件的pageoffice控件、WebOffice控件、国外的Office Viewer ActiveX Control
优点:
可在线编辑等。
缺点:
1、客户端需安装控件。
2、付费。
3、在Html5、CSS3以及桌面向浏览器转换的大潮流下,控件已是昨日黄花。
参考链接:
http://www.kehansoft.com/soaoffice/index.htm 
http://www.zhuozhengsoft.com/ 
http://www.officectrl.com/ 
http://www.anydraw.com/ 


五、微软的Office365

微软新出的在线文档,与Google文档抗衡,估计没谷歌文档,微软也懒得出这个
优点:
微软自家的东西原生态呈现。
缺点:
加载文件较多,各种图片、文字、样式、JQuery等,页面臃肿,加载速度慢,不适合手机预览
需要微软的批量许可(即授权),硬件投入方面:架设一台单独的服务器(可以是虚拟机),配置过低能安装,但无法运行,另外还需一台域服务器。而这两台机器上均不能安装其它程序,比如SQLServer,在Office365服务器上每次重启IIS会重置,也就是说你不能有任何其它网站。其主要是用来与SharePoint搭配使用。
参考链接:
http://technet.microsoft.com/zh-cn/library/jj219456(v=office.15).aspx 


六、第三方成熟的服务

如OfficeWeb365
优点:
1、OfficeWeb365采用适合中文排版的纯Html、CSS技术。
2、接口简单,适合PHP、JSP、ASP.net等所有的对接,省心省力。
3、费用低廉,节省投入。
4、不用关心客户端是否安装了Office软件,不用在客户端部署。
5、手机在线预览2页Word文档只有3K大小,且格式保留,领先全球的中文在线预览技术。
6、支持国产的金山WPS,这在国内尚属首列。
缺点:
1、OfficeWeb365只能查看不能编辑,目前在线编辑版的正在开发。
参考链接:
http://www.officeweb365.com 


七、在浏览器中直接打开

通过设置MiME类型,告诉浏览器这是Office文件,浏览器直接调用本地Office或PDF软件打开
优点:
1、不用编程,不用第三方服务,直截了当。
2、很多用户安装了Adobe的PDF预览软件,同时在浏览器上也直接安装了插件,浏览器可直接查看PDF文件。
缺点
你永远不知道客户机器上是否安装了Office软件,虽然几乎都安装了,但直接调用Office软件,客户体验大大下降,更何况还有个讨厌的迅雷一直在监视你的浏览器,不给你打开的机会,当然这些都是你无法预知的。


八、其它

如金山快写、一些网盘的预览
参考链接:
http://w.wps.cn/ 



已有 0人发表留言,猛击->> 这里<<-参与讨论


ITeye推荐



基于fabric和hg的自动化部署

$
0
0

自动化部署

fabric是个很好用的自动化部署工具,虽然功能比起puppet,saltstack之类要弱一些,但胜在用python,而且免安装服务端。

当然你要说docker更好我也同意,然而我是经常使用FreeBSD的,而且还有一些32位的低配系统,并不适合用docker。更不用说虚拟机了。

自动化部署的目的主要是简化手工部署的麻烦,包括初次安装部署和代码修改后的更新部署。初始部署主要是安装基础环境,初始化数据库等。更新部署则更麻烦一些,需要修改基础环境配置,变更数据库结构等。相比之下代码发布和更新反而是最简单的,用一个版本控制工具即可。

我是比较习惯用hg做代码版本管理的,当然这里要把hg换成git也可以,但我就是喜欢hg,你咬我啊。

fabric

fabric的官网是fabfile.org,基本用法都可以看官方文档,这里只对几种常用的用法作简单说明。

安装很简单,只要在本地用pip安装fabric即可,远程只要有SSH服务即可,不需要安装额外的东西,这点比puppet和saltstack省事。

使用上也很简单,最关键的是部署脚本是用python,比起puppet用的ruby来说,更合我口味。

默认的脚本文件名为: fabfile.py,当然你也可以用别的名字,但用起来就不方便了,类似make要用默认的Makefile文件名才方便一样。

执行默认脚本的命令为:fab <函数名>[<[参数名:]值>]

其中的函数名为fabfile.py中定义的任意函数(当使用@task装饰器时就只能使用已经装饰过的函数),也可以带上参数。对于特定主机或用户,还可以给函数加上角色装饰器。

运行本地命令的例子如下:

from fabric.api import local
def deploy_1():
    local(“hg commit”)
    local(“hg push”)
fab deploy_1

运行远端命令的例子如下:

from fabric.api import run
def deploy_2():
    run(“hg pull”)
    run(“hg update”)
fab deploy_2 -H hostname_or_ip

切换当前目录:

with lcd(“path”) # 本地目录
with cd(“path”) # 远端

错误处理:任何返回值不为0的操作都将导致异常,除非…

with settings(warn_only=True)

并且用命令的.failed属性来判断执行结果

env用于保存相关配置环境,比如SSH的KEY文件:key_filename,还有主机名列表:hosts,角色定义:roledefs等

角色的使用:

env.roledefs={‘role1’:[“user1@server1:port1”, “user2@server2:port2”],
    ‘role2’:[“user3@server3:port3”]}
@roles(“role1”)
def deploy_3():
    pass

结合fabric和hg的部署

以一个简单的python web应用为例来说明。

一次完整的手工初次部署大致包括以下内容(假设服务端系统已安装必要软件,比如hg, python, virtualenv, database, webserver,其中database和webserver已经配置好,单独的virtualenv已创建)等:

  • 在virtualenv环境中安装必要的依赖包 pip install -r requirements.txt
  • 通过hg发布要部署的代码 [local] hg push ssh://user@host/path; [remote] hg update
  • 初始化数据库
  • 启动(通过gunicorn, supervisord等)

代码修改过以后再次部署则涉及以下一些内容:

  • 更新依赖包
  • 更新代码
  • 更新数据库结构
  • 重启服务

再考虑到可能需要分别部署到测试环境和正式环境,又需要考虑以下问题:

  • 测试环境和正式环境涉及不一样的配置(比如连接不一样的数据库,配置不同的端口,甚至静态文件指向不同的路径)
  • 必须是测试环境中测试通过的版本才可以更新到正式环境中
  • 正式环境有更严格的权限管理(开发部门不可以直接部署到正式环境,甚至不能接触正式环境的配置信息)

由此,我们至少需要两个代码仓库:一个是开发代码库(repo_dev),包括测试配置,另一个是正式配置仓库(repo_prod)。

那么,基本的fabfile.py的deploy_dev函数大致有以下内容:

local("hg push repo_dev")
with cd("/target"):
    run("hg pull repo_dev")
    run("hg update")
    run("workon venv") # 切换到指定的virtualenv
    run("pip install -r requirements.txt")
    # 初始化数据库或更新数据库结构
    sudo("supervisorctl restart xxx") # gunicorn不能用supervisor重启,因为停止需要等待一段时间,建议用kill信号进行软重启

可以通过角色配置使repo_dev指定的远程服务器为测试主机 testhost ,在测试主机上测试通过以后,测试部分可以部署到正式机 prodhost 上。

with cd("/prodconf"):
    run("hg pull repo_prod")
with cd("/prod"):
    run("hg pull testhost") # 从测试主机上更新代码
    run("hg update rev") # 注意,这里要更新测试过的指定版本
    run("cp /prodconf/config .") # 使用正式配置替换测试配置
    run("workon venv")
    run("pip install -r requirements.txt")
    # 更新数据库结构
    sudo("supervisorctl restart xxx")

这个部署函数使用另一个用户角色,只要控制这个角色只有测试部门有权限即可,开发部门即使不慎运行了这个部署函数,也会因为没有权限而失败。

作者:Raptor 发表于2015/12/25 0:42:53 原文链接
阅读:140 评论:0 查看评论

[原]elasticsearch2.0对索引操作的一些优化

$
0
0

es2.0已经发布了,改进挺大的,对索引方面的优化的也挺多的。

持久化速率自动化
2.0之前es对于索引持久化到硬盘的速率默认是20mb一秒,这个值有时候会太小从而导致写入速度过慢从而影响索引速度。2.0对其进行了速率自动化的改进。当merge操作太慢时,会自动提高速率。当merge操作跟上来时再降低速率。这样会使突然间进行的大merge不至于占用整个节点的io从而影响到搜索和索引操作。

多数据文件目录支持
使用多个数据文件目录有两个好处,一是提高存储空间,二是提高io能力。
这个功能虽然之前的版本就有,但是原先的实现是基于lucene索引文件的,每次写索引文件时默认都放到空间更多的那个磁盘里,这样一个分片的数据可能分布在不同的磁盘里。这种方法的一个坏处就是当一个磁盘损坏时,在该磁盘有那怕一个索引文件的分片都会损坏。对于2.0来说,这个数据分布操作是以分片为单位的。也就是说当一个分片分配到这个节点时,节点会自动选择这个分片的索引文件分到那个磁盘目录,这样的话单个分片的索引文件只存在同一个磁盘,就算这个磁盘挂了也只是挂了该磁盘上的分片,其它磁盘的不受影响,并且多个磁盘也可以同时使用到。

默认使用doc_value属性
docvalue是lucene的新特性,它的作用是把字段的值存储在磁盘而不是在内存里,这样可以大大降低内存的使用减少gc的发生。对于2.0版,当一个字段设置为indexed并且为not analyzed时,es会自动帮它加上doc_value参数。

支持lucene5的BEST_COMPRESSION特性
BEST_COMPRESSION是lucene5的新索引压缩功能,默认lucene是使用LZ4压缩方式,使用BEST_COMPRESSION压缩的话压缩率更高,但在压缩时会损耗一些处理性能,也就是用处理能力换取存储空间。

对文档id的优化
在1.4.2版之前的es使用自动生成的id当出现网络问题时有可能出现重复的文档,现在进行了优化,每次都是update索引。并提高了通过id查询文档的性能。从1.4版本开始es默认生成的是Flake id而不是之前的uuid,主要是因为之前有人做过性能测试,Flake的文档查找性能比uuid的高一倍。

参考资料: 点击打开链接

作者:laigood12345 发表于2015/12/24 20:46:42 原文链接
阅读:4 评论:0 查看评论

Android单元测试研究与实践

$
0
0

Android单元测试介绍

处于高速迭代开发中的Android项目往往需要除黑盒测试外更加可靠的质量保障,这正是单元测试的用武之地。单元测试周期性对项目进行函数级别的测试,在良好的覆盖率下,能够持续维护代码逻辑,从而支持项目从容应对快速的版本更新。
单元测试是参与项目开发的工程师在项目代码之外建立的白盒测试工程,用于执行项目中的目标函数并验证其状态或者结果,其中,单元指的是测试的最小模块,通常指函数。如图1所示的绿色文件夹即是单元测试工程。这些代码能够检测目标代码的正确性,打包时单元测试的代码不会被编译进入APK中。


单元测试工程位置

图1 单元测试工程位置

与Java单元测试相同,Android单元测试也是维护代码逻辑的白盒工程,但由于Android运行环境的不同,Android单元测试的环境配置以及实施流程均有所不同。

Java单元测试

在传统Java单元测试中,我们需要针对每个函数进行设计单元测试用例。如图2便是一个典型的单元测试的用例。


单元测试示例

图2 单元测试示例

上述示例中,针对函数dosomething(Boolean param)的每个分支,我们都需要构造相应的参数并验证结果。单元测试的目标函数主要有三种:

  1. 有明确的返回值,如上图的dosomething(Boolean param),做单元测试时,只需调用这个函数,然后验证函数的返回值是否符合预期结果。
  2. 这个函数只改变其对象内部的一些属性或者状态,函数本身没有返回值,就验证它所改变的属性和状态。
  3. 一些函数没有返回值,也没有直接改变哪个值的状态,这就需要验证其行为,比如点击事件。

既没有返回值,也没有改变状态,又没有触发行为的函数是不可测试的,在项目中不应该存在。当存在同时具备上述多种特性时,本文建议采用多个case来真对每一种特性逐一验证,或者采用一个case,逐一执行目标函数并验证其影响。
构造用例的原则是测试用例与函数一对一,实现条件覆盖与路径覆盖。Java单元测试中,良好的单元测试是需要保证所有函数执行正确的,即所有边界条件都验证过,一个用例只测一个函数,便于维护。在Android单元测试中,并不要求对所有函数都覆盖到,像Android SDK中的函数回调则不用测试。

Android单元测试

在Android中,单元测试的本质依旧是验证函数的功能,测试框架也是JUnit。在Java中,编写代码面对的只有类、对象、函数,编写单元测试时可以在测试工程中创建一个对象出来然后执行其函数进行测试,而在Android中,编写代码需要面对的是组件、控件、生命周期、异步任务、消息传递等,虽然本质是SDK主动执行了一些实例的函数,但创建一个Activity并不能让它执行到resume的状态,因此需要JUnit之外的框架支持。
当前主流的单元测试框架AndroidTest和Robolectric,前者需要运行在Android环境上,后者可以直接运行在JVM上,速度也更快,可以直接由Jenkins周期性执行,无需准备Android环境。因此我们的单元测试基于Robolectric。对于一些测试对象依赖度较高而需要解除依赖的场景,我们可以借助Mock框架。

Android单元测试环境配置

Robolectric环境配置

Android单元测试依旧需要JUnit框架的支持,Robolectric只是提供了Android代码的运行环境。如果使用Robolectric 3.0,依赖配置如下:

testCompile 'junit:junit:4.10'
testCompile 'org.robolectric:robolectric:3.0'

Gradle对Robolectric 2.4的支持并不像3.0这样好,但Robolectric 2.4所有的测试框架均在一个包里,另外参考资料也比较丰富,作者更习惯使用2.4。如果使用Robolectric 2.4,则需要如下配置:

classpath 'org.robolectric:robolectric-gradle-plugin:0.14.+'//这行配置在buildscript的dependencies中
apply plugin: 'robolectric'
androidTestCompile 'org.robolectric:robolectric:2.4'

上述配置中,本文将testCompile写成androidTest,并且常见的Android工程的单元测试目录名称有test也有androidTest,这两种写法并没有功能上的差别,只是Android单元测试Test Artifact不同而已。Test Artifact如图3所示:


单元测试示例

图3 Test Artifact

在Gradle插件中,这两种Artifact执行的Task还是有些区别的,但是并不影响单元测试的写法与效果。虽然可以主动配置单元测试的项目路径,本文依旧建议采用与Test Artifact对应的项目路径和配置写法。

Mock配置

如果要测试的目标对象依赖关系较多,需要解除依赖关系,以免测试用例过于复杂,用Robolectric的Shadow是个办法,但是推荐更加简单的Mock框架,比如Mockito,该框架可以模拟出对象来,而且本身提供了一些验证函数执行的功能。Mockito配置如下:

repositories {
    jcenter()
}
dependencies {
    testCompile "org.mockito:mockito-core:1.+"
}

Robolectric使用介绍

Robolectric单元测试编写结构

单元测试代码写在项目的test(也可能是androidTest,该目录在项目中会呈浅绿色)目录下。单元测试也是一个标准的Java工程,以类为文件单位编写,执行的最小单位是函数,测试用例(以下简称case)是带有@Test注解的函数,单元测试里面带有case的类由Robolectric框架执行,需要为该类添加注解@RunWith(RobolectricTestRunner.class)。基于Robolectric的代码结构如下:

//省略一堆import
@RunWith(RobolectricTestRunner.class)
public class MainActivityTest {
    @Before
    public void setUp() {
        //执行初始化的操作
    }
    @Test
    public void testCase() {
        //执行各种测试逻辑判断
    }
}

上述结构中,带有@Before注解的函数在该类实例化后,会立即执行,通常用于执行一些初始化的操作,比如构造网络请求和构造Activity。带有@test注解的是单元测试的case,由Robolectric执行,这些case本身也是函数,可以在其他函数中调用,因此,case也是可以复用的。每个case都是独立的,case不会互相影响,即便是相互调用也不会存在多线程干扰的问题。

常见Robolectric用法

Robolectric支持单元测试范围从Activity的跳转、Activity展示View(包括菜单)和Fragment到View的点击触摸以及事件响应,同时Robolectric也能测试Toast和Dialog。对于需要网络请求数据的测试,Robolectric可以模拟网络请求的response。对于一些Robolectric不能测试的对象,比如ConcurrentTask,可以通过自定义Shadow的方式现实测试。下面将着重介绍Robolectric的常见用法。
Robolectric 2.4模拟网络请求
由于商业App的多数Activity界面数据都是通过网络请求获取,因为网络请求是大多数App首要处理的模块,测试依赖网络数据的Activity时,可以在@Before标记的函数中准备网络数据,进行网络请求的模拟。准备网络请求的代码如下:

public void prepareHttpResponse(String filePath) throws IOException {
        String netData = FileUtils.readFileToString(FileUtils.
            toFile(getClass().getResource(filePath)), HTTP.UTF_8);
        Robolectric.setDefaultHttpResponse(200, netData);
}//代码适用于Robolectric 2.4,3.0需要注意网络请求的包的位置

由于Robolectric 2.4并不会发送网络请求,因此需要本地创建网络请求所返回的数据,上述函数的filePath便是本地数据的文件的路径,setDefaultHttpResponse()则创建了该请求的Response。上述函数执行后,单元测试工程便拥有了与本地数据数据对应的网络请求,在这个函数执行后展示的Activity便是有数据的Activity。
在Robolectric 3.0环境下,单元测试可以发真的请求,并且能够请求到数据,本文依旧建议采用mock的办法构造网络请求,而不要依赖网络环境。
Activity展示测试与跳转测试
创建网络请求后,便可以测试Activity了。测试代码如下:

@Test
public void testSampleActivity(){
    SampleActivity sampleActivity=Robolectric.buildActivity(SampleActivity.class).
                create().resume().get();
    assertNotNull(sampleActivity);
    assertEquals("Activity的标题", sampleActivity.getTitle());
}

Robolectric.buildActivity()用于构造Activity,create()函数执行后,该Activity会运行到onCreate周期,resume()则对应onResume周期。assertNotNull和assertEquals是JUnit中的断言,Robolectric只提供运行环境,逻辑判断还是需要依赖JUnit中的断言。
Activity跳转是Android开发的重要逻辑,其测试方法如下:

@Test
public void testActivityTurn(ActionBarActivity firstActivity, Class secondActivity) {
    Intent intent = new Intent(firstActivity.getApplicationContext(), secondActivity);
    assertEquals(intent, Robolectric.shadowOf(firstActivity).getNextStartedActivity());//3.0的API与2.4不同
}

Fragment展示与切换
Fragment是Activity的一部分,在Robolectric模拟执行Activity过程中,如果触发了被测试的代码中的Fragment添加逻辑,Fragment会被添加到Activity中。
需要注意Fragment出现的时机,如果目标Activity中的Fragment的添加是执行在onResume阶段,在Activity被Robolectric执行resume()阶段前,该Activity中并不会出现该Fragment。采用Robolectric主动添加Fragment的方法如下:

@Test
public void addfragment(Activity activity, int fragmentContent){
    FragmentTestUtil.startFragment(activity.getSupportFragmentManager().findFragmentById(fragmentContent));
    Fragment fragment = activity.getSupportFragmentManager().findFragmentById(fragmentContent);
    assertNotNull(fragment);
}

startFragment()函数的主体便是常用的添加fragment的代码。切换一个Fragment往往由Activity中的代码逻辑完成,需要Activity的引用。
控件的点击以及可视验证

@Test
public void testButtonClick(int buttonID){
    Button submitButton = (Button) activity.findViewById(buttonID);
    assertTrue(submitButton.isEnabled());
    submitButton.performClick();
    //验证控件的行为
}

对控件的点击验证是调用performClick(),然后断言验证其行为。对于ListView这类涉及到Adapter的控件的点击验证,写法如下:

//listView被展示之后
listView.performItemClick(listView.getAdapter().getView(position, null, null), 0, 0);

与button等控件稍有不同。
Dialog和Toast测试
测试Dialog和Toast的方法如下:

public void testDialog(){
    Dialog dialog = ShadowDialog.getLatestDialog();
    assertNotNull(dialog);
}
public void testToast(String toastContent){
    ShadowHandler.idleMainLooper();
    assertEquals(toastContent, ShadowToast.getTextOfLatestToast());
}

上述函数均需要在Dialog或Toast产生之后执行,能够测试Dialog和Toast是否弹出。

Shadow写法介绍

Robolectric的本质是在Java运行环境下,采用Shadow的方式对Android中的组件进行模拟测试,从而实现Android单元测试。对于一些Robolectirc暂不支持的组件,可以采用自定义Shadow的方式扩展Robolectric的功能。

@Implements(Point.class)
public class ShadowPoint {
  @RealObject private Point realPoint;
  ...
  public void __constructor__(int x, int y) {
    realPoint.x = x;
    realPoint.y = y;
  }
}//样例来源于Robolectric官网

上述实例中,@Implements是声明Shadow的对象,@RealObject是获取一个Android 对象, constructor则是该Shadow的构造函数,Shadow还可以修改一些函数的功能,只需要在重载该函数的时候添加@Implementation,这种方式可以有效扩展Robolectric的功能。
Shadow是通过对真实的Android对象进行函数重载、初始化等方式对Android对象进行扩展,Shadow出来的对象的功能接近Android对象,可以看成是对Android对象一种修复。自定义的Shadow需要在config中声明,声明写法是@Config(shadows=ShadowPoint.class)。

Mock写法介绍

对于一些依赖关系复杂的测试对象,可以采用Mock框架解除依赖,常用的有Mockito。例如Mock一个List类型的对象实例,可以采用如下方式:

List list = mock(List.class);   //mock得到一个对象,也可以用@mock注入一个对象

所得到的list对象实例便是List类型的实例,如果不采用mock,List其实只是个接口,我们需要构造或者借助ArrayList才能进行实例化。与Shadow不同,Mock构造的是一个虚拟的对象,用于解耦真实对象所需要的依赖。Mock得到的对象仅仅是具备测试对象的类型,并不是真实的对象,也就是并没有执行过真实对象的逻辑。
Mock也具备一些补充JUnit的验证函数,比如设置函数的执行结果,示例如下:

When(sample.dosomething()).thenReturn(someAction);//when(一个函数执行).thenReturn(一个可替代真实函数的结果的返回值);
//上述代码是设置sample.dosomething()的返回值,当执行了sample.dosomething()这个函数时,就会得到someAction,从而解除了对真实的sample.dosomething()函数的依赖

上述代码为被测函数定义一个可替代真实函数的结果的返回值。当使用这个函数后,这个可验证的结果便会产生影响,从而代替函数的真实结果,这样便解除了对真实函数的依赖。
同时Mock框架也可以验证函数的执行次数,代码如下:

List list = mock(List.class);   //Mock得到一个对象
list.add(1);                    //执行一个函数
verify(list).add(1);            //验证这个函数的执行
verify(list,time(3)).add(1);    //验证这个函数的执行次数

在一些需要解除网络依赖的场景中,多使用Mock。比如对retrofit框架的网络依赖解除如下:

//代码参考了参考文献[3]
public class MockClient implements Client {
    @Override
    public Response execute(Request request) throws IOException {
        Uri uri = Uri.parse(request.getUrl());
        String responseString = "";
        if(uri.getPath().equals("/path/of/interest")) {
            responseString = "返回的json1";//这里是设置返回值
        } else {
            responseString = "返回的json2";
        }
        return new Response(request.getUrl(), 200, "nothing", Collections.EMPTY_LIST, new TypedByteArray("application/json", responseString.getBytes()));
    }
}
//MockClient使用方式如下:
RestAdapter.Builder builder = new RestAdapter.Builder();
builder.setClient(new MockClient());

这种方式下retrofit的response可以由单元测试编写者设置,而不来源于网络,从而解除了对网络环境的依赖。

在实际项目中使用Robolectric构建单元测试

单元测试的范围

在Android项目中,单元测试的对象是组件状态、控件行为、界面元素和自定义函数。本文并不推荐对每个函数进行一对一的测试,像onStart()、onDestroy()这些周期函数并不需要全部覆盖到。商业项目多采用Scrum模式,要求快速迭代,有时候未必有较多的时间写单元测试,不再要求逐个函数写单元测试。
本文单元测试的case多来源于一个简短的业务逻辑,单元测试case需要对这段业务逻辑进行验证。在验证的过程中,开发人员可以深度了解业务流程,同时新人来了看一下项目单元测试就知道哪个逻辑跑了多少函数,需要注意哪些边界——是的,单元测试需要像文档一样具备业务指导能力。
在大型项目中,遇到需要改动基类中代码的需求时,往往不能准确快速地知道改动后的影响范围,紧急时多采用创建子类覆盖父类函数的办法,但这不是长久之计,在足够覆盖率的单元测试支持下,跑一下单元测试就知道某个函数改动后的影响,可以放心地修改基类。
美团的Android单元测试编写流程如图4所示。


美团Android单元测试实施结构

图4 美团Android单元测试编写流程

单元测试最终需要输出文档式的单元测试代码,为线上代码提供良好的代码稳定性保证。

单元测试的流程

实际项目中,单元测试对象与页面是一对一的,并不建议跨页面,这样的单元测试藕合度太大,维护困难。单元测试需要找到页面的入口,分析项目页面中的元素、业务逻辑,这里的逻辑不仅仅包括界面元素的展示以及控件组件的行为,还包括代码的处理逻辑。然后可以创建单元测试case列表(列表用于纪录项目中单元测试的范围,便于单元测试的管理以及新人了解业务流程),列表中记录单元测试对象的页面,对象中的case逻辑以及名称等。工程师可以根据这个列表开始写单元测试代码。
单元测试是工程师代码级别的质量保证工程,上述流程并不能完全覆盖重要的业务逻辑以及边界条件,因此,需要写完后,看覆盖率,找出单元测试中没有覆盖到的函数分支条件等,然后继续补充单元测试case列表,并在单元测试工程代码中补上case。
直到规划的页面中所有逻辑的重要分支、边界条件都被覆盖,该项目的单元测试结束。单元测试流程如图5所示。


单元测试执行流程

图5 单元测试执行流程

上述分析页面入口所得到结果便是@Before标记的函数中的代码,之后的循环便是所有的case(@Test标记的函数)。

单元测试项目实践

为了系统的介绍单元测试的实施过程,本文创建了一个小型的demo项目作为测试对象。demo的功能是供用户发布所见的新闻到服务端,并浏览所有已经发表的新闻,是个典型的自媒体应用。该demo的开发和测试涉及到TextView、EditView、ListView、Button以及自定义View,包含了网络请求、多线程、异步任务以及界面跳转等。能够为多数商业项目提供参照样例。项目页面如图6所示。


单元测试case设计

图6 单元测试case设计

首先需要分析App的每个页面,针对页面提取出简短的业务逻辑,提取出的业务逻辑如图6绿色圈图所示。根据这些逻辑来设计单元测试的case(带有@Test注解的那个函数),这里的业务逻辑不仅指需求中的业务,还包括其他需要维护的代码逻辑。业务流程不允许跨页面,以免增加单元测试case的维护成本。针对demo中界面的单元测试case设计如下:

表1 单元测试case列表
目标页面业务覆盖界面元素逻辑描述最小断言数case名称
创建新闻页面
NewsCreatedActivity
编写新闻1.标题框
2.内容框
3.发布按钮
1.向标题框输入内容
2.向内容框输入内容
3.当标题和内容都存在的时候,上传按钮可点击
3testWriteNews()
输入新闻的金额1.Checkbox
2.金额控件
1.选中免费发布时,金额输入框消失
2.不选免费时可以输入金额
3.金额输入框只接受小数点后最多两位
3testValue()
菜单跳转至新闻列表1.菜单按钮1.点击菜单跳转到新闻列表页面1testMenuForTrunNewsList()
发布新闻1.发布按钮
2.Toast
1.当标题或者内容为空时,发布按钮不可点击
2.编写了新闻的前提下,点击发布按钮
3.新闻发布成功,弹出Toast提示 “新闻已提交”
4.没有标题或者内容时,新闻发布失败,弹出Toast提示“新闻提交失败”
5testNewsPush()、
testPushNewsFailed()
新闻列表页面
NewsListActivity
浏览新闻列表1.列表1.进入此页面后会出现新闻列表
2.有网络情况下,能发起网络请求
3.网络请求需要用Mock解除偶和,单独验证页面对数据的响应,后端返回一项时,列表只有一条数据
6testNewsListNoNetwork()、
testGetnewsWhenNetwork()、
testSetNews()
菜单跳转至创建新闻页面1.菜单按钮1.点击菜单跳转到创建新闻页面1testMenuForTrunCreatNews()
查看详细新闻1.有内容的列表
2.Dialog
1.有新闻的前提下,列表可点击,点击弹出Dialog1testNewsDialog()

接下来需要在单元测试工程中实现上述case,最小断言数是业务逻辑上的判断,并不是代码的边界条件,真实的case需要考虑代码的边界条件,比如数组为空等条件,因此,最终的断言数量会大于等于最小断言数。在需求业务上,最小断言数也是该需求的业务条件。
写完case后需要跑一遍单元测试并检查覆盖率报告,当覆盖率报告中缺少有些单元测试case列表中没有但是实际逻辑中会有的逻辑时,需要更新单元测试case列表,添加遗漏的逻辑,并将对应的代码补上。直到所有需要维护的逻辑都被覆盖,该项目中的单元测试才算完成。单元测试并不是QA的黑盒测试,需要保证对代码逻辑的覆盖。
对表1分析,第一个页面的“发布新闻”的case可以直接调用“编写新闻”的case,以满足条件“2.编写了新闻的前提下,点击发布按钮”,在JUnit框架下,case(带@Test注解的那个函数)也是个函数,直接调用这个函数就不是case,和case是无关的,两者并不会相互影响,可以直接调用以减少重复代码。第二个页面不同于第一个,一进入就需要网络请求,后续业务都需要依赖这个网络请求,单元测试不应该对某一个条件过度耦合,因此,需要用mock解除耦合,直接mock出网络请求得到的数据,单独验证页面对数据的响应。

总结

单元测试并不是一个能直接产生回报的工程,它的运行以及覆盖率也不能直接提升代码质量,但其带来的代码控制力能够大幅度降低大规模协同开发的风险。现在的商业App开发都是大型团队协作开发,不断会有新人加入,无论新人是刚入行的应届生还是工作多年,在代码存在一定业务耦合度的时候,修改代码就有一定风险,可能会影响之前比较隐蔽的业务逻辑,或者是丢失曾经的补丁,如果有高覆盖率的单元测试工程,就能很快定位到新增代码对现有项目的影响,与QA验收不同,这种影响是代码级的。
在本文所设计的单元测试流程中,单元测试的case和具体页面的具体业务流程以及该业务的代码逻辑紧密联系,单元测试如同技术文档一般,能够体现出一个业务逻辑运行了多少函数,需要注意什么样的条件。这是一种新人了解业务流程、对业务进行代码级别融入的好办法,看一下以前的单元测试case,就能知道与该case对应的那个页面上的那个业务逻辑会执行多少函数,以及这些函数可能出现的结果。

参考文献

[1] http://robolectric.org
[2] https://github.com/square/okhttp/tree/master/mockwebserver
[3] http://stackoverflow.com/questions/17544751/square-retrofit-server-mock-for-testing
[4] https://en.wikipedia.org/wiki/Unit_testing

移动数据 | 流量跑的快,运营商偷你流量了?别搞笑了好么

$
0
0

359013216

199IT数据中心微信账户:i199IT

“天价流量”正牵动各方神经。“4小时耗23G流量”、“一夜跑流量50G”、“WLAN流量一晚产生1000G”……一系列流量“疯跑”事件让运营商“受千夫所指”,信任危机加速蔓延,坊间甚至流传着“4G忘了关房子就归移动”的说法。运营商当真练就了一身“顺手牵流量”的超群技艺?究竟流量是怎么产生的?又该如何测算?为此,本报记者采访了通信专业博士、大学副教授、新浪微博知名博主(@奥卡姆剃刀)张弛先生。

流量G时代到来

流量“疯跑”将运营商捧为了热搜红角儿。面对一起又一起“天价流量”事件,人们在震惊之余恐怕还难回过神来。当真溜走了这么多流量?莫不是一场集体错觉?不,是流量G时代真的到来了!

受4G移动电话用户快速增长、4G套餐资费不断下调等影响,移动互联网接入流量消费持续呈现爆发式增长。10月份通信行业运行数据显示,10月当月移动互联网接入流量达4.2亿G,创历史新高。

张弛感叹:4G速度太快了!4G时代,流量“哗啦啦”地跑,几秒钟就能跑几十兆,开一个晚上的热点就能跑完《琅琊榜》全剧的流量。根据国际电信联盟的专家预测,5G速率可达几十G,届时人均月流量将达到36T,“流量”将越来越超越人们的认知。他表示,作为信息社会价值流动的基础,流量将创造难以估量的价值。

运营商“偷流量”在技术层面是伪命题

接二连三的“偷流量”风波中,运营商成为众矢之的,究竟是“冤大头”、“背锅侠”,还是确有蹊跷?

张弛认为,从技术角度上分析,运营商绝不可能造假“偷流量”:

  • 第一,我国运营商的流量计费,普遍采用华为、中兴、爱立信等电信设备商的计量硬件设备,而非运营商自己捣鼓的,这些设备的技术规格不论在国内还是国外均是统一的,运营商基本上不可能动手脚。
  • 第二,运营商关于流量的计量、计费是两个独立的系统,利用电信设备商提供的设备对流量进行测算,得出数据后再推送到计费系统里。即使是人为地增多计量,比如把2个流量算成3个,也得不到任何实质性好处。
  • 第三,运营商是一个庞大的、统一的系统,如果要在流量上“动手脚”,至少涉及一两百号知情人,纸能包得住火?
  • 第四,如果运营商铁了心要“偷流量”获取不当利益,也不会傻到只偷几个用户的吧?如果全面撒网,每个用户都偷一点,岂不是更有赚头?近期曝出的知名案例,实际上都是随机的个例,存在特殊性和偶然性,而事件真相也佐证了运营商是“背锅侠”这一说法。
  • 第五,在诺基亚功能机时代,手机说明书一般多达200来页,看几个小时都看不完;到了安卓/苹果时代,手机比以前更为复杂了,然而说明书却基本失踪了。在这种情况下,即使是高级知识分子也可能存在对电信业运行机理以及对手机设置和对流量控制的认知不足,毕竟隔行如隔山,更不用说普通文化水平的用户了。举个例子,微博打假名人王海曾在微博上与我讨论流量话题,说其一个月“跑”了很多流量,但从他的截图可发现,他所使用的苹果手机,同时打开了局域网助理和系统服务,这种情况下,当WiFi信号弱的时候手机便会自动打开蜂窝网,与此同时,IOS系统升级也会通过跑流量进行,而他却浑然不知。王海甚至表示,运营商偷没偷流量“看秤”就一目了然。这个想法很朴实,在菜市场“看秤”就知道是否缺斤短两,但电信的计费计量是非常复杂的问题,国家标准实际上是公开的,只是百姓并不看得懂。

综观而言,移动互联网时代,科技越来越超越了普通人的理解能力。近期网上流传的“流量偷跑”案例,最终都无一例外地发生了“反转”,但普通老百姓还在误解、错怪运营商。当然,这并不能埋怨普通用户“不懂行”,这是时代的割裂,就像是父母辈中的许多人不大会使用打车软件等新兴的移动互联网产品一样。

流量如何计费?

尽管运营商通过还原一出出流量“偷跑”乌龙闹剧的真相自证了清白,但消费者的疑虑和情绪化表达仍难以消除,究其根本,普通用户对流量消费缺乏认识、对运营商的习惯性质疑等是主要原因。

不可否认,电信资费计费过程存在复杂性,普通用户若想一探究竟,搞清楚流量费是如何产生的,须得先明晰运营商是如何计费的。

实际上,运营商的流量计费大致囊括流量话单的产生、流量计费、用户上网流量轨迹记录这三大环节。首先,当用户手机接入互联网时,用户手机与运营商的网络设备经过认证后将建立会话连接。由此,网络设备便会记录用户的上网行为并生成计费话单,话单内容包括上网时间、流量、位置信息等。紧接着,生成的话单将传递给计费系统。计费系统把话单导入数据库,根据用户的套餐资费进行批价计费,先扣除用户的免费流量,再从用户余额中扣除实时流量费用。倘或流量或者余额不足,计费系统则会发出短信提醒。

值得一提的是,相较3G时代网络设备在用户断网后才产生话单,4G时代网络设备的话单生成更为灵活,时间限定(如上网达1小时)、流量限定(达50MB)、无线切换等皆可触发话单的生成。这也是近期几个案例中用户都是在短时间内(如一夜之间、3小时)惊觉“流量暴走”的原因所在。

通信信息报

您可能也喜欢的文章:

移动数据时代运维的转型之道

全球运营商集体遭遇市场饱和 流量经营受考验

全流量时代,运营商如何应对OTT

运营商做互联网金融亮点不多 到底靠谱么?

流量不清零不如把处置权交到用户的手里
无觅

mysql性能优化-慢查询分析、优化索引和配置

$
0
0

目录

一、优化概述

二、查询与索引优化分析

1性能瓶颈定位

Show命令

慢查询日志

explain分析查询

profiling分析查询

 

2索引及查询优化

三、配置优化

1)      max_connections

2)      back_log

3)      interactive_timeout

4)      key_buffer_size

5)      query_cache_size

6)      record_buffer_size

7)      read_rnd_buffer_size

8)      sort_buffer_size

9)      join_buffer_size

10)    table_cache

11)    max_heap_table_size

12)    tmp_table_size

13)    thread_cache_size

14)    thread_concurrency

15)    wait_timeout

 

一、 优化概述

MySQL 数据库是常见的两个瓶颈是CPU和I/O的瓶颈,CPU在饱和的时候一般发生在数据装入内存或从磁盘上读取数据时候。磁盘I/O瓶颈发生在装入数据远大于 内存容量的时候,如果应用分布在网络上,那么查询量相当大的时候那么平瓶颈就会出现在网络上,我们可以用mpstat, iostat, sar和vmstat来查看系统的性能状态。

除了服务器硬件的性能瓶颈,对于MySQL系统本身,我们可以使用工具来优化数据库的性能,通常有三种:使用索引,使用EXPLAIN分析查询以及调整MySQL的内部配置。

二、查询与索引优化分析

在优化MySQL时,通常需要对数据库进行分析,常见的分析手段有慢查询日志,EXPLAIN 分析查询,profiling分析以及show命令查询系统状态及系统变量,通过定位分析性能的瓶颈,才能更好的优化数据库系统的性能。

1 性能瓶颈定位Show命令

我们可以通过show命令查看MySQL状态及变量,找到系统的瓶颈:

Mysql> show status ——显示状态信息(扩展show status like ‘XXX’)

Mysql> show variables ——显示系统变量(扩展show variables like ‘XXX’)

Mysql> show innodb status ——显示InnoDB存储引擎的状态

Mysql> show processlist ——查看当前SQL执行,包括执行状态、是否锁表等

Shell> mysqladmin variables -u username -p password——显示系统变量

Shell> mysqladmin extended-status -u username -p password——显示状态信息

查看状态变量及帮助:

Shell> mysqld –verbose –help [|more #逐行显示]

 

比较全的Show命令的使用可参考: http://blog. phpbean.com/a.cn/18/

慢查询日志

慢查询日志开启:

在配置文件my.cnf或my.ini中在[ mysqld]一行下面加入两个配置参数

log-slow-queries=/data/mysqldata/slow-query.log           

long_query_time=2                                                                 

注:log-slow-queries参数为慢查询日志存放的位置,一般这个目录要有mysql的运行帐号的可写权限,一般都将这个目录设置为mysql的数据存放目录;

long_query_time=2中的2表示查询超过两秒才记录;

在my.cnf或者my.ini中添加log-queries-not-using-indexes参数,表示记录下没有使用索引的查询。

log-slow-queries=/data/mysqldata/slow-query.log           

long_query_time=10                                                               

log-queries-not-using-indexes                                             

慢查询日志开启方法二:

我们可以通过命令行设置变量来即时启动慢日志查询。由下图可知慢日志没有打开,slow_launch_time=# 表示如果建立线程花费了比这个值更长的时间,slow_launch_threads 计数器将增加

设置慢日志开启

MySQL后可以查询long_query_time 的值 。

 

为了方便测试,可以将修改慢查询时间为5秒。

慢查询分析mysqldumpslow

我们可以通过打开log文件查看得知哪些SQL执行效率低下

[root@localhost mysql]# more slow-query.log                            

# Time: 081026 19:46:34                                                                          

# User@Host: root[root] @ localhost []                                                           

# Query_time: 11 Lock_time: 0 Rows_sent: 1 Rows_examined: 6552961        

select count(*) from t_user;                                                                                

从日志中,可以发现查询时间超过5 秒的SQL,而小于5秒的没有出现在此日志中。

如果慢查询日志中记录内容很多,可以使用mysqldumpslow工具(MySQL客户端安装自带)来对慢查询日志进行分类汇总。mysqldumpslow对日志文件进行了分类汇总,显示汇总后摘要结果。

进入log的存放目录,运行

[root@mysql_data]#mysqldumpslow  slow-query.log                                 

Reading mysql slow query log from slow-query.log                            

Count: 2 Time=11.00s (22s) Lock=0.00s (0s) Rows=1.0 (2), root[root]@mysql    

select count(N) from t_user;                                                

mysqldumpslow命令

/path/mysqldumpslow -s c -t 10 /database/mysql/slow-query.log                      

这会输出记录次数最多的10条SQL语句,其中:

-s, 是表示按照何种方式排序,c、t、l、r分别是按照记录次数、时间、查询时间、返回的记录数来排序,ac、at、al、ar,表示相应的倒叙;

-t, 是top n的意思,即为返回前面多少条的数据;

-g, 后边可以写一个正则匹配模式,大小写不敏感的;

例如:

/path/mysqldumpslow -s r -t 10 /database/mysql/slow-log                                 

得到返回记录集最多的10个查询。

/path/mysqldumpslow -s t -t 10 -g “left join” /database/mysql/slow-log       

得到按照时间排序的前10条里面含有左连接的查询语句。

使 用mysqldumpslow命令可以非常明确的得到各种我们需要的查询语句,对MySQL查询语句的监控、分析、优化是MySQL优化非常重要的一步。 开启慢查询日志后,由于日志记录操作,在一定程度上会占用CPU资源影响mysql的性能,但是可以阶段性开启来定位性能瓶颈。

explain分析查询

使用 EXPLAIN 关键字可以模拟优化器执行SQL查询语句,从而知道MySQL是如何处理你的SQL语句的。这可以帮你分析你的查询语句或是表结构的性能瓶颈。通过explain命令可以得到:

– 表的读取顺序

– 数据读取操作的操作类型

– 哪些索引可以使用

– 哪些索引被实际使用

– 表之间的引用

– 每张表有多少行被优化器查询

EXPLAIN字段:

ØTable:显示这一行的数据是关于哪张表的

Øpossible_keys:显示可能应用在这张表中的索引。如果为空,没有可能的索引。可以为相关的域从WHERE语句中选择一个合适的语句

Økey:实际使用的索引。如果为NULL,则没有使用索引。MYSQL很少会选择优化不足的索引,此时可以在SELECT语句中使用USE INDEX(index)来强制使用一个索引或者用IGNORE INDEX(index)来强制忽略索引

Økey_len:使用的索引的长度。在不损失精确性的情况下,长度越短越好

Øref:显示索引的哪一列被使用了,如果可能的话,是一个常数

Ørows:MySQL认为必须检索的用来返回请求数据的行数

Øtype:这是最重要的字段之一,显示查询使用了何种类型。从最好到最差的连接类型为system、const、eq_reg、ref、range、index和ALL

nsystem、const:可以将查询的变量转为常量.  如id=1; id为 主键或唯一键.

neq_ref:访问索引,返回某单一行的数据.(通常在联接时出现,查询使用的索引为主键或惟一键)

nref:访问索引,返回某个值的数据.(可以返回多行) 通常使用=时发生

nrange:这个连接类型使用索引返回一个范围中的行,比如使用>或<查找东西,并且该字段上建有索引时发生的情况(注:不一定好于index)

nindex:以索引的顺序进行全表扫描,优点是不用排序,缺点是还要全表扫描

nALL:全表扫描,应该尽量避免

ØExtra:关于MYSQL如何解析查询的额外信息,主要有以下几种

nusing index:只用到索引,可以避免访问表. 

nusing where:使用到where来过虑数据. 不是所有的where clause都要显示using where. 如以=方式访问索引.

nusing tmporary:用到临时表

nusing filesort:用到额外的排序. (当使用order by v1,而没用到索引时,就会使用额外的排序)

nrange checked for eache record(index map:N):没有好的索引.

 

profiling分析查询

通过慢日志查询可以知道哪些SQL语句执行效率低下,通过explain我们可以得知SQL语句的具体执行情况,索引使用等,还可以结合show命令查看执行状态。

如果觉得explain的信息不够详细,可以同通过profiling命令得到更准确的SQL执行消耗系统资源的信息。

profiling默认是关闭的。可以通过以下语句查看

 

 

打开功能: mysql>set profiling=1; 执行需要测试的sql 语句:

mysql> show profiles\G; 可以得到被执行的SQL语句的时间和ID

mysql>show profile for query 1; 得到对应SQL语句执行的详细信息

Show Profile命令格式:

SHOW PROFILE [type [, type] … ]                                    

    [FOR QUERY n]                                                            

    [LIMIT row_count [OFFSET offset]]                             

type:                                                                                  

    ALL                                                                               

  | BLOCK IO                                                                      

  | CONTEXT SWITCHES                                                   

  | CPU                                                                              

  | IPC                                                                                

  | MEMORY                                                                            

  | PAGE FAULTS                                                               

  | SOURCE                                                                        

  | SWAPS                

 

 

 

 

以 上的16rows是针对非常简单的select语句的资源信息,对于较复杂的SQL语句,会有更多的行和字段,比如converting HEAP to MyISAM 、Copying to tmp table等等,由于以上的SQL语句不存在复杂的表操作,所以未显示这些字段。通过profiling资源耗费信息,我们可以采取针对性的优化措施。

 

测试完毕以后 ,关闭参数:mysql> set profiling=0

 

 

2     索引及查询优化

 

索引的类型

Ø 普通索引:这是最基本的索引类型,没唯一性之类的限制。

Ø 唯一性索引:和普通索引基本相同,但所有的索引列值保持唯一性。

Ø 主键:主键是一种唯一索引,但必须指定为”PRIMARY KEY”。

Ø 全文索引:MYSQL从3.23.23开始支持全文索引和全文检索。在MYSQL中,全文索引的索引类型为FULLTEXT。全文索引可以在VARCHAR或者TEXT类型的列上创建。

大多数MySQL索引(PRIMARY KEY、UNIQUE、INDEX和FULLTEXT)使用B树中存储。空间列类型的索引使用R-树,MEMORY表支持hash索引。

单列索引和多列索引(复合索引)

索引可以是单列索引,也可以是多列索引。对相关的列使用索引是提高SELECT操作性能的最佳途径之一。

多列索引:

MySQL可以为多个列创建索引。一个索引可以包括15个列。对于某些列类型,可以索引列的左前缀,列的顺序非常重要。

多列索引可以视为包含通过连接索引列的值而创建的值的排序的数组。一般来说,即使是限制最严格的单列索引,它的限制能力也远远低于多列索引。

最左前缀

多列索引有一个特点,即最左前缀(Leftmost Prefixing)。假如有一个多列索引为key(firstname lastname age),当搜索条件是以下各种列的组合和顺序时,MySQL将使用该多列索引:

firstname,lastname,age

firstname,lastname

firstname

也就是说,相当于还建立了key(firstname lastname)和key(firstname)。

索引主要用于下面的操作:

Ø 快速找出匹配一个WHERE子句的行。

Ø 删除行。当执行联接时,从其它表检索行。

Ø 对 具体有索引的列key_col找出MAX()或MIN()值。由预处理器进行优化,检查是否对索引中在key_col之前发生所有关键字元素使用了 WHERE key_part_# = constant。在这种情况下,MySQL为每个MIN()或MAX()表达式执行一次关键字查找,并用常数替 换它。如果所有表达式替换为常量,查询立即返回。例如:

SELECT MIN(key2), MAX (key2)  FROM tb WHERE key1=10;

Ø 如果对一个可用关键字的最左面的前缀进行了排序或分组(例如,ORDER BY key_part_1,key_part_2),排序或分组一个表。如果所有关键字元素后面有DESC,关键字以倒序被读取。

Ø 在一些情况中,可以对一个查询进行优化以便不用查询数据行即可以检索值。如果查询只使用来自某个表的数字型并且构成某些关键字的最左面前缀的列,为了更快,可以从索引树检索出值。

SELECT key_part3 FROM tb WHERE key_part1=1

有 时MySQL不使用索引,即使有可用的索引。一种情形是当优化器估计到使用索引将需要MySQL访问表中的大部分行时。(在这种情况下,表扫描可能会更快 些)。然而,如果此类查询使用LIMIT只搜索部分行,MySQL则使用索引,因为它可以更快地找到几行并在结果中返回。例如:

 

合理的建立索引的建议:

(1)  越小的数据类型通常更好:越小的数据类型通常在磁盘、内存和CPU缓存中都需要更少的空间,处理起来更快。 

(2)  简单的数据类型更好:整型数据比起字符,处理开销更小,因为字符串的比较更复杂。在MySQL中,应该用内置的日期和时间数据类型,而不是用字符串来存储时间;以及用整型数据类型存储IP地址。

(3)  尽量避免NULL:应该指定列为NOT NULL,除非你想存储NULL。在MySQL中,含有空值的列很难进行查询优化,因为它们使得索引、索引的统计信息以及比较运算更加复杂。你应该用0、一个特殊的值或者一个空串代替空值

 

这部分是关于索引和写SQL语句时应当注意的一些琐碎建议和注意点。

1. 当结果集只有一行数据时使用LIMIT 1

2. 避免SELECT *,始终指定你需要的列

从表中读取越多的数据,查询会变得更慢。他增加了磁盘需要操作的时间,还是在数据库服务器与WEB服务器是独立分开的情况下。你将会经历非常漫长的网络延迟,仅仅是因为数据不必要的在服务器之间传输。

3. 使用连接(JOIN)来代替子查询(Sub-Queries)

       连接(JOIN).. 之所以更有效率一些,是因为MySQL不需要在内存中创建临时表来完成这个逻辑上的需要两个步骤的查询工作。

4. 使用ENUMCHAR 而不是VARCHAR,使用合理的字段属性长度

5. 尽可能的使用NOT NULL

6. 固定长度的表会更快

7. 拆分大的DELETE INSERT 语句

8. 查询的列越小越快

 

 Where条件

在查询中,WHERE条件也是一个比较重要的因素,尽量少并且是合理的where条件是很重要的,尽量在 多个条件的时候,把会提取尽量少数据量的条件放在前面,减少后一个where条件的查询时间。

有些where条件会导致索引无效:

Ø where子句的查询条件里有!=,MySQL将无法使用索引。

Ø where子句使用了Mysql函数的时候,索引将无效,比如:select * from tb where left(name, 4) = ‘xxx’

Ø 使用LIKE进行搜索匹配的时候,这样 索引是有效的:select * from tbl1 where name like ‘xxx%’,而like ‘%xxx%’ 时索引无效

 

三、    配置优化

安 装MySQL后,配置文件my.cnf在 /MySQL安装目录/share/mysql目录中,该目录中还包含多个配置文件可供参考,有my-large.cnf ,my-huge.cnf,  my-medium.cnf,my-small.cnf,分别对应大中小型数据库应用的配置。win环境下即存在于MySQL安装目录中的.ini文 件。

 

下面列出了对性能优化影响较大的主要变量,主要分为连接请求的变量和缓冲区变量。

1.   连接请求的变量:

1)     max_connections

MySQL 的最大连接数,增加该值增加mysqld 要求的文件描述符的数量。如果服务器的并发连接请求量比较大,建议调高此值,以增加并行连接数量,当然这建立在机器能支撑的情况下,因为如果连接数越多, 介于MySQL会为每个连接提供连接缓冲区,就会开销越多的内存,所以要适当调整该值,不能盲目提高设值。

数值过小会经常出现ERROR 1040: Too many connections错误,可以过’conn%’通配符查看当前状态的连接数量,以定夺该值的大小。

show variables like ‘max_connections’ 最大连接数

show  status like ‘max_used_connections’响应的连接数

如下:

mysql> show variables like ‘max_connections‘;

+———————–+——-+

| Variable_name | Value |

+———————–+——-+

| max_connections | 256  |

+———————–+——-+

mysql> show status like ‘max%connections‘;

+———————–+——-+

| Variable_name       | Value |

+—————————-+——-+

| max_used_connections | 256|

+—————————-+——-+

max_used_connections / max_connections * 100% (理想值≈ 85%) 

如果max_used_connections跟max_connections相同 那么就是max_connections设置过低或者超过服务器负载上限了,低于10%则设置过大。

2)     back_log

MySQL 能暂存的连接数量。当主要MySQL线程在一个很短时间内得到非常多的连接请求,这就起作用。如果MySQL的连接数据达到 max_connections时,新来的请求将会被存在堆栈中,以等待某一连接释放资源,该堆栈的数量即back_log,如果等待连接的数量超过 back_log,将不被授予连接资源。

back_log值指出在MySQL暂时停止回答新请求之前的短时间内有多少个请求可以被存在堆栈中。只有如果期望在一个短时间内有很多连接,你需要增加它,换句话说,这值对到来的TCP/IP连接的侦听队列的大小。

当 观察你主机进程列表(mysql> show full processlist),发现大量264084 | unauthenticated user | xxx.xxx.xxx.xxx | NULL | Connect | NULL | login | NULL 的待连接进程时,就要加大back_log 的值了。

默认数值是50,可调优为128,对于 Linux系统设置范围为小于512的整数。 

3)     interactive_timeout

一个交互连接在被服务器在关闭前等待行动的秒数。一个交互的客户被定义为对mysql_real_connect()使用CLIENT_INTERACTIVE 选项的客户。 

默认数值是28800,可调优为7200。 

2.   缓冲区变量

全局缓冲:

4)     key_buffer_size

key_buffer_size 指定索引缓冲区的大小,它决定索引处理的速度,尤其是索引读的速度。通过检查状态值Key_read_requests和Key_reads,可以知道 key_buffer_size设置是否合理。比例key_reads / key_read_requests应该尽可能的低,至少是1:100,1:1000更好(上述状态值可以使用SHOW STATUS LIKE ‘key_read%’获得)。

key_buffer_size只对MyISAM表起作用。即使你不使用MyISAM表,但是内部的临时磁盘表是MyISAM表,也要使用该值。可以使用检查状态值created_tmp_disk_tables得知详情。

举例如下:

mysql> show variables like ‘key_buffer_size‘;

+——————-+————+

| Variable_name | Value      |

+———————+————+

| key_buffer_size | 536870912 |

+———— ———-+————+

key_buffer_size为512MB,我们再看一下key_buffer_size的使用情况:

mysql> show global status like ‘key_read%‘;

+————————+————-+

| Variable_name   | Value    |

+————————+————-+

| Key_read_requests| 27813678764 |

| Key_reads   |  6798830      |

+————————+————-+

一共有27813678764个索引读取请求,有6798830个请求在内存中没有找到直接从硬盘读取索引,计算索引未命中缓存的概率:

key_cache_miss_rate =Key_reads / Key_read_requests * 100%,设置在1/1000左右较好

默认配置数值是8388600(8M),主机有4GB内存,可以调优值为268435456(256MB)。

5)     query_cache_size

使用查询缓冲,MySQL将查询结果存放在缓冲区中,今后对于同样的SELECT语句(区分大小写),将直接从缓冲区中读取结果。

通 过检查状态值Qcache_*,可以知道query_cache_size设置是否合理(上述状态值可以使用SHOW STATUS LIKE ‘Qcache%’获得)。如果Qcache_lowmem_prunes的值非常大,则表明经常出现缓冲不够的情况,如果Qcache_hits的值也 非常大,则表明查询缓冲使用非常频繁,此时需要增加缓冲大小;如果Qcache_hits的值不大,则表明你的查询重复率很低,这种情况下使用查询缓冲反 而会影响效率,那么可以考虑不用查询缓冲。此外,在SELECT语句中加入SQL_NO_CACHE可以明确表示不使用查询缓冲。

 

与查询缓冲有关的参数还有query_cache_type、query_cache_limit、query_cache_min_res_unit。

 

query_cache_type指定是否使用查询缓冲,可以设置为0、1、2,该变量是SESSION级的变量。

query_cache_limit指定单个查询能够使用的缓冲区大小,缺省为1M。

query_cache_min_res_unit 是在4.1版本以后引入的,它指定分配缓冲区空间的最小单位,缺省为4K。检查状态值Qcache_free_blocks,如果该值非常大,则表明缓冲 区中碎片很多,这就表明查询结果都比较小,此时需要减小query_cache_min_res_unit。

举例如下:

mysql> show global status like ‘qcache%‘;

+——————————-+—————–+

| Variable_name                  | Value        |

+——————————-+—————–+

| Qcache_free_blocks        | 22756       |

| Qcache_free_memory     | 76764704    |

| Qcache_hits           | 213028692 |

| Qcache_inserts         | 208894227   |

| Qcache_lowmem_prunes   | 4010916      |

| Qcache_not_cached | 13385031    |

| Qcache_queries_in_cache | 43560 |

| Qcache_total_blocks          | 111212      |

+——————————-+—————–+

mysql> show variables like ‘query_cache%‘;

+————————————–+————–+

| Variable_name            | Value      |

+————————————–+———–+

| query_cache_limit         | 2097152     |

| query_cache_min_res_unit      | 4096    |

| query_cache_size         | 203423744 |

| query_cache_type        | ON           |

| query_cache_wlock_invalidate | OFF   |

+————————————–+—————+

查询缓存碎片率= Qcache_free_blocks / Qcache_total_blocks * 100%

如果查询缓存碎片率超过20%,可以用FLUSH QUERY CACHE整理缓存碎片,或者试试减小query_cache_min_res_unit,如果你的查询都是小数据量的话。

查询缓存利用率= (query_cache_size – Qcache_free_memory) / query_cache_size * 100%

查询缓存利用率在25%以下的话说明query_cache_size设置的过大,可适当减小;查询缓存利用率在80%以上而且Qcache_lowmem_prunes > 50的话说明query_cache_size可能有点小,要不就是碎片太多。

查询缓存命中率= (Qcache_hits – Qcache_inserts) / Qcache_hits * 100%

示例服务器查询缓存碎片率=20.46%,查询缓存利用率=62.26%,查询缓存命中率=1.94%,命中率很差,可能写操作比较频繁吧,而且可能有些碎片。

每个连接的缓冲

6)    record_buffer_size

每个进行一个顺序扫描的线程为其扫描的每张表分配这个大小的一个缓冲区。如果你做很多顺序扫描,你可能想要增加该值。

默认数值是131072(128K),可改为16773120 (16M)

7)     read_rnd_buffer_size

随 机读缓冲区大小。当按任意顺序读取行时(例如,按照排序顺序),将分配一个随机读缓存区。进行排序查询时,MySQL会首先扫描一遍该缓冲,以避免磁盘搜 索,提高查询速度,如果需要排序大量数据,可适当调高该值。但MySQL会为每个客户连接发放该缓冲空间,所以应尽量适当设置该值,以避免内存开销过大。

一般可设置为16M 

8)     sort_buffer_size

每个需要进行排序的线程分配该大小的一个缓冲区。增加这值加速ORDER BY或GROUP BY操作。

默认数值是2097144(2M),可改为16777208 (16M)。

9)     join_buffer_size

联合查询操作所能使用的缓冲区大小

record_buffer_size,read_rnd_buffer_size,sort_buffer_size,join_buffer_size为每个线程独占,也就是说,如果有100个线程连接,则占用为16M*100

10)  table_cache

表高速缓存的大小。每当MySQL访问一个表时,如果在表缓冲区中还有空间,该表就被打开并放入其中,这样可以更快地访问表内容。 通过检查峰值时间的状态值Open_tables Opened_tables ,可以决定是否需要增加table_cache 的值。如 果你发现open_tables等于table_cache,并且opened_tables在不断增长,那么你就需要增加table_cache的值了 (上述状态值可以使用SHOW STATUS LIKE ‘Open%tables’获得)。注意,不能盲目地把table_cache设置成很大的值。如果设置得太高,可能会造成文件描述符不足,从而造成性能 不稳定或者连接失败。

1G内存机器,推荐值是128-256。内存在4GB左右的服务器该参数可设置为256M或384M。

11)  max_heap_table_size

用户可以创建的内存表(memory table)的大小。这个值用来计算内存表的最大行数值。这个变量支持动态改变,即set @max_heap_table_size=#

这个变量和tmp_table_size一起限制了内部内存表的大小。如果某个内部heap(堆积)表大小超过tmp_table_size,MySQL可以根据需要自动将内存中的heap表改为基于硬盘的MyISAM表。

12)  tmp_table_size

通过设置tmp_table_size选项来增加一张临时表的大小,例如做高级GROUP BY操作生成的临时表。如果调高该值,MySQL同时将增加heap表的大小,可达到提高联接查询速度的效果, 建议尽量优化查询,要确保查询过程中生成的临时表在内存中,避免临时表过大导致生成基于硬盘的MyISAM表

mysql> show global status like ‘created_tmp%‘;

+——————————–+———+

| Variable_name             | Value |

+———————————-+———+

| Created_tmp_disk_tables | 21197  |

| Created_tmp_files   | 58  |

| Created_tmp_tables  | 1771587 |

+——————————–+———–+

每 次创建临时表,Created_tmp_tables增加,如果临时表大小超过tmp_table_size,则是在磁盘上创建临时 表,Created_tmp_disk_tables也增加,Created_tmp_files表示MySQL服务创建的临时文件文件数,比较理想的配 置是:

Created_tmp_disk_tables / Created_tmp_tables * 100% <= 25%比如上面的服务器Created_tmp_disk_tables / Created_tmp_tables * 100% =1.20%,应该相当好了

默认为16M,可调到64-256最佳,线程独占,太大可能内存不够I/O堵塞

13)  thread_cache_size

可以复用的保存在中的线程的数量。如果有,新的线程从缓存中取得,当断开连接的时候如果有空间,客户的线置在缓存中。如果有很多新的线程,为了提高性能可以这个变量值。

通过比较 Connections和Threads_created状态的变量,可以看到这个变量的作用。

默认值为110,可调优为80。 

14)  thread_concurrency

推荐设置为服务器 CPU核数的2倍,例如双核的CPU, 那么thread_concurrency的应该为4;2个双核的cpu, thread_concurrency的值应为8。默认为8

15)  wait_timeout

指定一个请求的最大连接时间,对于4GB左右内存的服务器可以设置为5-10。

 

3.    配置InnoDB的几个变量

innodb_buffer_pool_size

对于InnoDB表来说,innodb_buffer_pool_size的作用就相当于key_buffer_size对于MyISAM表的作用一样。InnoDB使用该参数指定大小的内存来缓冲数据和索引。对于单独的MySQL 数据库服务器,最大可以把该值设置成物理内存的80%。

根据MySQL手册,对于2G内存的机器,推荐值是1G(50%)。

 

innodb_flush_log_at_trx_commit

主 要控制了innodb将log buffer中的数据写入日志文件并flush磁盘的时间点,取值分别为0、1、2三个。0,表示当事务提交时,不做日志写入操作,而是每秒钟将log buffer中的数据写入日志文件并flush磁盘一次;1,则在每秒钟或是每次事物的提交都会引起日志文件写入、flush磁盘的操作,确保了事务的 ACID;设置为2,每次事务提交引起写入日志文件的动作,但每秒钟完成一次flush磁盘操作。

实际测试发现,该值对插入数据的速度影响非常大,设置为2时插入10000条记录只需要2秒,设置为0时只需要1秒,而设置为1时则需要229秒。因此,MySQL手册也建议尽量将插入操作合并成一个事务,这样可以大幅提高速度。

根据MySQL手册,在允许丢失最近部分事务的危险的前提下,可以把该值设为0或2。

 

innodb_log_buffer_size

log缓存大小,一般为1-8M,默认为1M,对于较大的事务,可以增大缓存大小。

可设置为4M或8M。

 

innodb_additional_mem_pool_size

该参数指定InnoDB用来存储数据字典和其他内部数据结构的内存池大小。缺省值是1M。通常不用太大,只要够用就行,应该与表结构的复杂度有关系。如果不够用,MySQL会在错误日志中写入一条警告信息。

根据MySQL手册,对于2G内存的机器,推荐值是20M,可适当增加。

 

innodb_thread_concurrency=8

推荐设置为 2*(NumCPUs+NumDisks),默认一般为8



已有 0人发表留言,猛击->> 这里<<-参与讨论


ITeye推荐



微評六家互联网公司抵制流量劫持的联合声明

$
0
0
1、六家公司呼吁有关运营商严格打击流量劫持问题。分为域名劫持和数据劫持两类,但未说明流量劫持可能实施人,劫持人可能有两类,一是运营商及其授权机构,二是第三方公司。
2、劫持行为侵犯了用户和服务商的利益,声明标题中也用了“违法”字样,可能更有效的渠道不是发布公开声明,而是到主管部门投诉。
3、主管部门是原工信部还是已经划到了国网办,我不知道。

附:六家声明:  

 

教育信息化、信息孤岛与身份认证

$
0
0

Design programs to be connected to other programs --Unix philosophy

缘起

最近接触的项目和需求中,统一身份认证的问题反复出现,花了不少功夫去了解身份认证这块相关的标准和协议。

身份认证/授权这部分涉及的概念真是五花八门,一度把我搞得七荤八素,相关概念包括但不限于:session,cookie,OpenID,OAuth2,CAS,JWT,SSO,Token,SAML,Shibboleth(以上这些概念并不都在同一层面)

其中一些属于协议,一些属于实现,一些属于通用的概念。

最近在和某高校合作的项目中,校方也痛下决心要把校园内各个系统进行打通(教育部有要求避免信息孤岛),在交流中发现,信息孤岛在国内高校中广泛存在,而我自己目前折腾的东西属于教育信息化这个领域,所以觉得相比于技术细节,理清这些问题是更有意义的。

问题描述

我们首先来看看我们面临的问题

许多高校每年采购若干教育信息化系统,再加上内部自建的,积年累月,大浪淘下之后,学校里运行着若干异构系统,他们都有一套自己的认证机制,自己的用户系统,某天学校有了新需求,需要若干系统协同合作,却发现整合他们的成本已经高于购置一套新系统的成本(包括时间成本)。

于是他们用新购的系统解决眼前的业务问题,接着这个新系统风风火火地奔往下一个信息孤岛

这些教育信息化系统/教务系统进校之初,往往需要先与教务相关的数据中心整合,同步用户以及其他关系,和许多工程项目一样,为了进度采用一种dirty and quick的方案。不同公司的不同系统,与学校数据中心的整合又往往不一样,于是校方整出许多数据视图和接口,应对一直只需,没精力做长期打算,这些临时接口往往是滋生bug,产生臃肿代码,引起错误和需要大量重复劳动的地方。

问题分析

这些问题的出现,几乎是一种必然。我们知道几乎所有的系统都需要登录访问,访问是有状态的,所以各个系统需要与数据中心整合(获取用户信息),而整合过程中,由于业务的压力,人们往往倾向于一种quick的方案,dirty与否并不再考量范围,更遑论架构上的长远考虑。

缺乏标准,临时方案,追赶进度,于是盐水越喝越多,越来越渴。

我觉得解开这团乱码的关键是身份认证与授权,放弃临时方案,而采用一些被广泛采用而健壮灵活的开放标准。在初期架构上花些精力,一劳永逸地解决这些问题。当然由于这些设计的完备和周到,他们也允许最大非侵入式地整合既有系统,尽可能少地干预以及投入使用的系统,是认证层尽量透明。前提是校方真的有决心去推荐这件事

当然,这个问题倒并不只在高校出现了,企业中也是广泛存在的。实际上只要纯在异构系统,统一身份的需求就很可能出现,由于 懒惰是程序员的美德(这是个玩笑,程序员三大美德里的懒惰当然不是这种愚蠢的懒惰啦),dirty and quick的临时方案就层出不穷

思路

我认为上述的这些问题,可以把它们视为分布式系统的身份认证问题。

而在高校中,教务系统(数据库)往往作为认证权威,不同于OAuth解决的分布式认证问题(去中心化),高校信息化系统的身份认证问题可以被简化为集中式身份认证

标准和协议的意义

有了标准和协议,我们就避免了不必要的争论,而将精力放到真正的问题上。

比如在大括号是否换行这类问题上,你很难说谁的做法更好,所以换行的一派对待不换行的一派,一贯的做法就是手持火把以异教徒的眼光看待对方。但各执己见的结果是项目整体上的一致性很差。一旦有了标准,许多无谓的争论(而且不可能有结果)就可以避免,这在公司之间的协作上很有意义,否则谁也不愿服谁的方案,而且是任意一方的方案逻辑上都能救得一时之急。

在选择标准和协议的时候,我们最好尽量选择被业界广泛使用的,这样一来,不仅易于整合进其他优秀的系统(国外许多优秀的系统都会特意说明兼容这些标准,国内这种做法还不多,但据我所知最近已经有一些教育行业的公司开始或准备这么做了)

采用标准和协议的另一个好处是,许多常见的漏洞可以被避免,这些协议经过广泛的使用,大多的坑都被踩过了,后来者们不仅容易免费获得优秀的实现(诸如CAS有 Jasig/cas)。我们不必重造车轮,就能获得安全可靠的解决方案,这要比拍脑袋的临时方案健壮得多

想想我自己之前踩过的坑,多数时候都可以用

读书太少而想得太多来

来解释

同时标准和协议是一种可增量式改良的实体,由于这些协议和标准的开放性,用户在使用过程遇到的任何问题,都能被收集与反馈,最后标准和协议被不断完善。它们像生命体一样不断地成长与健壮

解决方案

顺着身份认证的思路,具体的解决方案倒有很多可选的,诸如CAS,JWT,SAML等,这些具体的协议与解决方案就留在后续文章里来讨论啦。

在下篇文章里,我们会关注耶鲁大学贡献了CAS协议(CAS是一个协议,并不限于具体语言实现),该协议在国外高校中广泛使用,Open edX就天然支持这种协议,通过该协议,我们轻松就将Open edX与教务系统整合了

也正是这个经历,让我尝到了标准和协议的甜头,才决定写这一系列的文章

另外

身份认证并不足以消除信息孤岛,但会是关键的一步

在折腾Open edX的过程中,我发现RESTful API也是极其有力的工具,通过让系统对外暴露RESTful风格的接口,系统之间变得协作友好,它们如同有了生命体一般,这正是《Unix编程艺术》里建议的,考虑尽量让程序彼此之间能通信,让程序具有组合性,用清晰的接口把若干简单的模块组合成一个复杂的软件。这样是应对复杂度极好的策略

系统之间的协作,如同你培育的生态球一般,各个生物既互相依存/共生又彼此独立,能量和物质顺着食物链流动,它们形成一个生态系统

在协作紧密的地方,信息是流通顺畅的,系统之间分工协作,友好相处。信息孤岛就被打破了

而系统之间的协作,统一的身份往往是最初需要迈出的一步

利用OpenCV的人脸检测给头像带上圣诞帽

$
0
0

原图:

 

效果:

 

 

 

    原理其实很简单:

采用一张圣诞帽的png图像作为素材,

 

   

    利用png图像背景是透明的,贴在背景图片上就是戴帽子的效果了。

人脸检测的目的主要是为了确定贴帽子的位置,类似ps中自由变换的功能,检测到人脸中间的位置,resize圣诞帽子和人脸大小匹配,确定位置,贴上去,ok!

 

 

 

代码:非常简洁,根据参考博客给出的代码,由OpenCV自带的人脸检测代码经过简单修改即可。

// getheader.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"

#include "opencv2/objdetect/objdetect.hpp"
#include "opencv2/highgui/highgui.hpp"
#include "opencv2/imgproc/imgproc.hpp"

#include <iostream>
#include <stdio.h>

using namespace std;
using namespace cv;


#pragma comment(lib,"opencv_core2410d.lib")                
#pragma comment(lib,"opencv_highgui2410d.lib")                
//#pragma comment(lib,"opencv_objdetect2410d.lib")   
#pragma comment(lib,"opencv_imgproc2410d.lib")  

/** Function Headers */
void detectAndDisplay( Mat frame );

/** Global variables */
//-- Note, either copy these two files from opencv/data/haarscascades to your current folder, or change these locations
String face_cascade_name = "D:\\Program Files\\opencv\\sources\\data\\haarcascades\\haarcascade_frontalface_alt.xml";
String eyes_cascade_name = "D:\\Program Files\\opencv\\sources\\data\\haarcascades\\haarcascade_eye_tree_eyeglasses.xml";
CascadeClassifier face_cascade;
CascadeClassifier eyes_cascade;
string window_name = "Capture - Face detection";
RNG rng(12345);

const int FRAME_WIDTH = 1280;
const int FRAME_HEIGHT = 240;
/**
* @function main
*/
int main( void )
{
	CvCapture* capture;
	//VideoCapture capture;
	Mat frame;

	//-- 1. Load the cascades
	if( !face_cascade.load( face_cascade_name ) ){ printf("--(!)Error loading\n"); return -1; };
	if( !eyes_cascade.load( eyes_cascade_name ) ){ printf("--(!)Error loading\n"); return -1; };

			frame = imread("19.jpg");//背景图片

			//-- 3. Apply the classifier to the frame
			if( !frame.empty() )
			{ detectAndDisplay( frame ); }
			
			waitKey(0);
	
	return 0;
}

void mapToMat(const cv::Mat &srcAlpha, cv::Mat &dest, int x, int y)
{
	int nc = 3;
	int alpha = 0;

	for (int j = 0; j < srcAlpha.rows; j++)
	{
		for (int i = 0; i < srcAlpha.cols*3; i += 3)
		{
			alpha = srcAlpha.ptr<uchar>(j)[i / 3*4 + 3];
			//alpha = 255-alpha;
			if(alpha != 0) //4通道图像的alpha判断
			{
				for (int k = 0; k < 3; k++)
				{
					// if (src1.ptr<uchar>(j)[i / nc*nc + k] != 0)
					if( (j+y < dest.rows) && (j+y>=0) &&
						((i+x*3) / 3*3 + k < dest.cols*3) && ((i+x*3) / 3*3 + k >= 0) &&
						(i/nc*4 + k < srcAlpha.cols*4) && (i/nc*4 + k >=0) )
					{
						dest.ptr<uchar>(j+y)[(i+x*nc) / nc*nc + k] = srcAlpha.ptr<uchar>(j)[(i) / nc*4 + k];
					}
				}
			}
		}
	}
}

/**
* @function detectAndDisplay
*/
void detectAndDisplay( Mat frame )
{
	std::vector<Rect> faces;
	Mat frame_gray;
	Mat hatAlpha;

	hatAlpha = imread("2.png",-1);//圣诞帽的图片

	cvtColor( frame, frame_gray, COLOR_BGR2GRAY );
	equalizeHist( frame_gray, frame_gray );
	//-- Detect faces
	face_cascade.detectMultiScale( frame_gray, faces, 1.1, 2, 0|CV_HAAR_SCALE_IMAGE, Size(30, 30) );

	for( size_t i = 0; i < faces.size(); i++ )
	{

		Point center( faces[i].x + faces[i].width/2, faces[i].y + faces[i].height/2 );
		// ellipse( frame, center, Size( faces[i].width/2, faces[i].height/2), 0, 0, 360, Scalar( 255, 0, 255 ), 2, 8, 0 );

		// line(frame,Point(faces[i].x,faces[i].y),center,Scalar(255,0,0),5);

		Mat faceROI = frame_gray( faces[i] );
		std::vector<Rect> eyes;

		//-- In each face, detect eyes
		eyes_cascade.detectMultiScale( faceROI, eyes, 1.1, 2, 0 |CV_HAAR_SCALE_IMAGE, Size(30, 30) );

		for( size_t j = 0; j < eyes.size(); j++ )
		{
			Point eye_center( faces[i].x + eyes[j].x + eyes[j].width/2, faces[i].y + eyes[j].y + eyes[j].height/2 );
			int radius = cvRound( (eyes[j].width + eyes[j].height)*0.25 );
			// circle( frame, eye_center, radius, Scalar( 255, 0, 0 ), 3, 8, 0 );
		}

		// if(eyes.size())
		{
			resize(hatAlpha,hatAlpha,Size(faces[i].width, faces[i].height),0,0,INTER_LANCZOS4);
			// mapToMat(hatAlpha,frame,center.x+2.5*faces[i].width,center.y-1.3*faces[i].height);
			mapToMat(hatAlpha,frame,faces[i].x,faces[i].y-0.8*faces[i].height);
		}
	}
	//-- Show what you got
	imshow( window_name, frame );
	imwrite("merry christmas.jpg",frame);
}


 

 

 

 

 

 

参考文献:

http://blog.csdn.net/lonelyrains/article/details/50388999

http://docs.opencv.org/doc/tutorials/objdetect/cascade_classifier/cascade_classifier.html

作者:wangyaninglm 发表于2015/12/24 21:47:00 原文链接
阅读:164 评论:0 查看评论

流量劫持这种事 不靠求运营商就能用技术解决问题吗?

$
0
0

有时候你在用手机浏览网页甚至打开 App 的时候(比如打开微信公众号文章或者打开手机淘宝),有时候会出现一个广告弹窗,甚至有时候是运营商自己的流量提醒,这个广告有时候和 App 的内容和类型完全不符,不了解情况的用户很可能会怪罪 App 乱弹广告,也许你真的是怪错人了,你的流量可能被某些机构劫持了。

今天,今日头条、美团 - 大众点评网、360、腾讯、微博、小米科技六家公司发表联合声明,共同呼吁有关运营商严格打击流量劫持问题,重视互联网被流量劫持可能导致的严重后果。

联合声明指出,在当前的移动互联网环境下,流量劫持主要分为两种方式: 域名劫持和数据劫持 ,放任流量劫持会导致扰乱市场秩序、损害用户利益以及传播诈骗、色情等低俗甚至严重违法信息的恶果。

对于流量劫持这种事情已经成为业界非常普遍但是又无可奈何的一件事情,主要在于不敢得罪运营商,现在终于到了几家大型互联网公司联合起来「声明」的地步, 那么对于这种流氓手法真的没有办法 吗?为了我们咨询了 阿里云网络方面的资深工程师亭林。

相对于 PC 端的网络环境,移动端的网络环境更为复杂,2G、3G、4G、Wi-Fi 各有不同,而复杂的网络环境也增加了流量劫持的可能性和复杂程度。

流量劫持的方式主要分为两种,域名劫持和数据劫持。

域名劫持是针对传统 DNS 解析的常见劫持方式。用户在浏览器输入网址,即发出一个 HTTP 请求,首先需要进行域名解析,得到业务服务器的 IP 地址。使用传统 DNS 解析时,会通过当地网络运营商提供的 Local DNS 解析得到结果。 域名劫持,即是在请求 Local DNS 解析域名时出现问题,目标域名被恶意地解析到其他 IP 地址,造成用户无法正常使用服务。

解决域名劫持的一个办法就是绕开 Local DNS,通过一个可信的源头来解析域名,解析方式不需要拘泥于 DNS 协议,也可以通过 HTTP 的方式。亭林介绍道两年前,手机淘宝等 APP 也曾遇到这一问题,随后在做底层网络优化时, 通过使用自己定制的 HTTPDNS,一个安全可信的域名解析方案,解决了域名劫持问题,现在 & nbsp;HTTPDNS 技术也准备通过阿里云开放给广大开发者使用,当前这款产品正在内测中,预期将在明年初上线。

数据劫持基本针对明文传输的内容发生。用户发起 HTTP 请求,服务器返回页面内容时,经过中间网络,页面内容被篡改或加塞内容, 强行插入弹窗或者广告。

行业内解决的办法即是对内容进行 HTTPS 加密 ,实现密文传输,彻底避免劫持问题。

MD5 校验同样能起到防止数据劫持的作用 ,MD5 校验是指内容返回前,应用层对返回的数据进行校验,生成校验值;同时,内容接收方接收到内容后,也对内容进行校验,同样生成校验值,将这两个校验值进行比对,倘若一致,则可以判断数据无劫持。 但相比 HTTPS 加密,MD5 校验存在一定风险,劫持方技术能力强则有可能在篡改内容后替换校验值,导致接收方判断错误。

HTTPS 一开始是以加密通信为需求而诞生的,第一批用户也是银行等金融机构。但随着互联网上个人数据传输变得更加普遍,HTTPS 早已经成为了互联网行业的大势所趋。 今年双 11,阿里的淘宝、天猫、聚划算等电商平台就做到了全站的 HTTPS 加密访问,当然这也是开放的。

* 题图来自日本动画片《Pokemon》


Openlayers中热力图的实现

$
0
0

概述:

本文讲述结合heatmap.js,在Openlayers中如何实现热力图。


heatmap.js简介:

Heatmap 是用来呈现一定区域内的统计度量,最常见的网站访问热力图就是以特殊高亮的形式显示访客热衷的页面区域和访客所在的地理区域的图示。Heatmap.js 这个 JavaScript 库可以实现各种动态热力图的网页,帮助您研究和可视化用户的行为。


实现效果:

实现代码:

<html><head><meta charset="UTF-8"><title>heatmap.js OpenLayers Heatmap Layer</title><link rel="stylesheet" href="../../../plugin/OpenLayers-2.13.1/theme/default/style.css" type="text/css"><style>
		html, body, #map{
			padding:0;
			margin:0;
			height:100%;
			width:100%;
			overflow: hidden;
		}</style><script src="../../../plugin/OpenLayers-2.13.1/OpenLayers.js"></script><script type="text/javascript" src="extend/heatmap.js"></script><script type="text/javascript" src="extend/heatmap-openlayers.js"></script><script type="text/javascript">
		var map, layer, heatmap;
		function init(){
			var testData={
				max: 5,
				data: [
					{name:"乌鲁木齐",lat:43.782225,lon:87.576079,count:1},
					{name:"拉萨",lat:29.71056,lon:91.163218,count:1},
					{name:"西宁",lat:36.593725,lon:101.797439,count:1},
					{name:"兰州",lat:36.119175,lon:103.584421,count:2},
					{name:"成都",lat:30.714315,lon:104.035634,count:3},
					{name:"重庆",lat:29.479073,lon:106.519225,count:4},
					{name:"贵阳",lat:26.457486,lon:106.668183,count:2},
					{name:"昆明",lat:24.969568,lon:102.726915,count:2},
					{name:"银川",lat:38.598593,lon:106.167324,count:2},
					{name:"西安",lat:34.276221,lon:108.967213,count:3},
					{name:"南宁",lat:22.748502,lon:108.234036,count:3},
					{name:"海口",lat:19.97015,lon:110.346274,count:3},
					{name:"广州",lat:23.183277,lon:113.226755,count:4},
					{name:"长沙",lat:28.170082,lon:112.947996,count:4},
					{name:"南昌",lat:28.652529,lon:115.893762,count:4},
					{name:"福州",lat:26.070956,lon:119.246798,count:4},
					{name:"台北",lat:25.008476,lon:121.503585,count:2},
					{name:"杭州",lat:30.330742,lon:120.183062,count:4},
					{name:"上海",lat:31.253514,lon:121.449713,count:5},
					{name:"武汉",lat:30.579401,lon:114.216652,count:5},
					{name:"合肥",lat:31.838495,lon:117.262334,count:3},
					{name:"南京",lat:32.085164,lon:118.805714,count:4},
					{name:"郑州",lat:34.746419,lon:113.651151,count:4},
					{name:"济南",lat:36.608511,lon:117.048354,count:4},
					{name:"石家庄",lat:38.033361,lon:114.478253,count:4},
					{name:"太原",lat:37.798488,lon:112.483119,count:3},
					{name:"呼和浩特",lat:40.895807,lon:111.842856,count:3},
					{name:"天津",lat:38.925801,lon:117.351108,count:4},
					{name:"沈阳",lat:41.801674,lon:123.29626,count:3},
					{name:"长春",lat:43.982041,lon:125.261357,count:4},
					{name:"哈尔滨",lat:45.693857,lon:126.567056,count:3},
					{name:"北京",lat:39.892297,lon:116.068297,count:5},
					{name:"香港",lat:22.428066,lon:114.093184,count:2},
					{name:"澳门",lat:22.18471,lon:113.552554,count:1}
				]
			};
			var transformedTestData = { max: testData.max , data: [] },
					data = testData.data,
					datalen = data.length,
					nudata = [];
			// in order to use the OpenLayers Heatmap Layer we have to transform our data into
			// { max: <max>, data: [{lonlat: <OpenLayers.LonLat>, count: <count>},...]}
			while(datalen--){
				nudata.push({
					lonlat: new OpenLayers.LonLat(data[datalen].lon, data[datalen].lat),
					count: data[datalen].count
				});
			}
			transformedTestData.data = nudata;
			var format = 'image/png';
			var bounds = new OpenLayers.Bounds(
					73.45100463562233, 18.16324718764174,
					134.97679764650596, 53.531943152223576
			);
			var options = {
				controls: [],
				maxExtent: bounds,
				maxResolution: 0.2403351289487642,
				projection: "EPSG:4326",
				units: 'degrees'
			};
			map = new OpenLayers.Map('map', options);
			var tiled = new OpenLayers.Layer.WMS("Geoserver layers - Tiled","http://localhost:8088/geoserver/lzugis/wms",
					{"LAYERS": 'province',"STYLES": '',
						format: format
					},
					{
						buffer: 0,
						displayOutsideMaxExtent: true,
						isBaseLayer: true,
						yx : {'EPSG:4326' : true}
					}
			);
			OpenLayers.INCHES_PER_UNIT["千米"] = OpenLayers.INCHES_PER_UNIT["km"];
			OpenLayers.INCHES_PER_UNIT["米"] = OpenLayers.INCHES_PER_UNIT["m"];
			OpenLayers.INCHES_PER_UNIT["英里"] = OpenLayers.INCHES_PER_UNIT["mi"];
			OpenLayers.INCHES_PER_UNIT["英寸"] = OpenLayers.INCHES_PER_UNIT["ft"];
			//比例尺
			map.addControl(new OpenLayers.Control.ScaleLine({topOutUnits:"千米",topInUnits:"米",bottomOutUnits:"英里",
				bottomInUnits:"英寸"
			}));
			map.addControl(new OpenLayers.Control.Zoom());
			map.addControl(new OpenLayers.Control.Navigation());
			map.addControl(new OpenLayers.Control.OverviewMap());

			// create our heatmap layer
			heatmap = new OpenLayers.Layer.Heatmap( "Heatmap Layer",
					map, tiled,
					{
						visible: true,
						radius:10
					},
					{
						isBaseLayer: false,
						opacity: 0.3,
						projection: new OpenLayers.Projection("EPSG:4326")
					}
			);
			map.addLayers([tiled,heatmap]);
			map.zoomToExtent(bounds);
			console.log(transformedTestData);
			heatmap.setDataSet(transformedTestData);
		}</script></head><body onload="init()"><div id="map"></div></body></html>

附件:

heatmap-openlayers.js
/* 
 * heatmap.js OpenLayers Heatmap Class
 *
 * Copyright (c) 2011, Patrick Wied (http://www.patrick-wied.at)
 * Dual-licensed under the MIT (http://www.opensource.org/licenses/mit-license.php)
 * and the Beerware (http://en.wikipedia.org/wiki/Beerware) license.
 * 
 * Modified on Jun,06 2011 by Antonio Santiago (http://www.acuriousanimal.com)
 * - Heatmaps as independent map layer.
 * - Points based on OpenLayers.LonLat.
 * - Data initialization in constructor.
 * - Improved 'addDataPoint' to add new lonlat based points.
 */ 
OpenLayers.Layer.Heatmap = OpenLayers.Class(OpenLayers.Layer, {
	// the heatmap isn't a basic layer by default - you usually want to display the heatmap over another map ;)
	isBaseLayer: false,
	heatmap: null,
	mapLayer: null,
	// we store the lon lat data, because we have to redraw with new positions on zoomend|moveend
	tmpData: {},
        initialize: function(name, map, mLayer, hmoptions, options){
            var heatdiv = document.createElement("div"),
                handler;

            OpenLayers.Layer.prototype.initialize.apply(this, [name, options]);

	    heatdiv.style.cssText = "position:absolute;width:"+map.size.w+"px;height:"+map.size.h+"px;";
	    // this will be the heatmaps element
	    this.div.appendChild(heatdiv);
	    // add to our heatmap.js config
	    hmoptions.element = heatdiv;
	    this.mapLayer = mLayer;
	    this.map = map;
            // create the heatmap with passed heatmap-options
	    this.heatmap = h337.create(hmoptions);

            handler = function(){ 
                if(this.tmpData.max){
                    this.updateLayer(); 
                }
            };
	    // on zoomend and moveend we have to move the canvas element and redraw the datapoints with new positions
	    map.events.register("zoomend", this, handler);
	    map.events.register("moveend", this, handler);
        },
	updateLayer: function(){
                var pixelOffset = this.getPixelOffset(),
                    el = this.heatmap.get('element');
                // if the pixeloffset e.g. for x was positive move the canvas element to the left by setting left:-offset.y px 
                // otherwise move it the right by setting it a positive value. same for top
                el.style.top = ((pixelOffset.y > 0)?('-'+pixelOffset.y):(Math.abs(pixelOffset.y)))+'px';
                el.style.left = ((pixelOffset.x > 0)?('-'+pixelOffset.x):(Math.abs(pixelOffset.x)))+'px';
                this.setDataSet(this.tmpData);
	},
        getPixelOffset: function () {
            var o = this.mapLayer.map.layerContainerOrigin,
                o_lonlat = new OpenLayers.LonLat(o.lon, o.lat),
                o_pixel = this.mapLayer.getViewPortPxFromLonLat(o_lonlat),
                c = this.mapLayer.map.center,
                c_lonlat = new OpenLayers.LonLat(c.lon, c.lat),
                c_pixel = this.mapLayer.getViewPortPxFromLonLat(c_lonlat);

            return { 
                x: o_pixel.x - c_pixel.x,
                y: o_pixel.y - c_pixel.y 
            };

        },
	setDataSet: function(obj){
	    var set = {},
		dataset = obj.data,
		dlen = dataset.length,
                entry, lonlat, pixel;

		set.max = obj.max;
		set.data = [];
		// get the pixels for all the lonlat entries
            while(dlen--){
                entry = dataset[dlen],
                lonlat = entry.lonlat.clone().transform(this.projection, this.map.getProjectionObject()),
                pixel = this.roundPixels(this.getViewPortPxFromLonLat(lonlat));
                    
                if(pixel){
                    set.data.push({x: pixel.x, y: pixel.y, count: entry.count});
                }
            }
	    this.tmpData = obj;
	    this.heatmap.store.setDataSet(set);
	},
	// we don't want to have decimal numbers such as xxx.9813212 since they slow canvas performance down + don't look nice
	roundPixels: function(p){
	    if(p.x < 0 || p.y < 0){
	        return false;
            }
            p.x = (p.x >> 0);
	    p.y = (p.y >> 0);
            return p;
	},
	// same procedure as setDataSet
	addDataPoint: function(lonlat){
	    var pixel = this.roundPixels(this.mapLayer.getViewPortPxFromLonLat(lonlat)),
                entry = {lonlat: lonlat},
                args;

            if(arguments.length == 2){
                entry.count = arguments[1];
            }

            this.tmpData.data.push(entry);
            
            if(pixel){
                args = [pixel.x, pixel.y];

		if(arguments.length == 2){
		    args.push(arguments[1]);
		}
		this.heatmap.store.addDataPoint.apply(this.heatmap.store, args);
	    }

	},
	toggle: function(){
		this.heatmap.toggleDisplay();
	},
	destroy: function() {
        // for now, nothing special to do here. 
        OpenLayers.Layer.Grid.prototype.destroy.apply(this, arguments);  
    },
	CLASS_NAME: "OpenLayers.Layer.Heatmap"
});






作者:GISShiXiSheng 发表于2015/12/26 11:15:54 原文链接
阅读:66 评论:0 查看评论

高并发服务端分布式系统设计概要

$
0
0

写这篇文章的目的,主要是把今年以来学习的一些东西积淀下来,同时作为之前文章《 高性能分布式计算与存储系统设计概要》的补充与提升,然而本人水平非常有限,回头看之前写的文章也有许多不足,甚至是错误,希望同学们看到了错误多多见谅,更欢迎与我讨论并指正。

我大概是从2010年底起开始进入高并发、高性能服务器和分布式这一块领域的研究,到现在也差不多有三年,但其实很多东西仍然是一知半解,我所提到的许许多多概念,也许任何一个我都不能讲的很清楚,还需要继续钻研。但我们平时在工作和学习中,多半也只能从这种一知半解开始,慢慢琢磨,不断改进。

好了,下面开始说我们今天要设计的系统。

这个系统的目标很明确,针对千万级以上PV的网站,设计一套用于后台的高并发的分布式处理系统。这套系统包含业务逻辑的处理、各种计算、存储、日志、备份等方面内容,可用于类微博,SNS,广告推送,邮件等有大量线上并发请求的场景。

如何抗大流量高并发?(不要告诉我把服务器买的再好一点)说起来很简单,就是“分”,如何“分”,简单的说就是把不同的业务分拆到不同的服务器上去跑(垂直拆分),相同的业务压力分拆到不同的服务器去跑(水平拆分),并时刻不要忘记备份、扩展、意外处理等讨厌的问题。说起来都比较简单,但设计和实现起来,就会比较困难。以前我的文章,都是“从整到零”的方式来设计一个系统,这次咱们就反着顺序来。

那我们首先来看,我们的数据应该如何存储和取用。根据我们之前确定的“分”的方法,先确定以下2点:

(1)我们的分布式系统,按不同的业务,存储不同的数据;(2)同样的业务,同一个数据应存储多份,其中有的存储提供读写,而有的存储只提供读。

好,先解释下这2点。对于(1)应该容易理解,比如说,我这套系统用于微博(就假想我们做一个山寨的推特吧,给他个命名就叫“山推” 好了,以下都叫山推,Stwi),那么,“我关注的人”这一个业务的数据,肯定和“我发了的推文”这个业务的数据是分开存储的,那么我们现在把,每一个业务所负责的数据的存储,称为一个group。即以group的方式,来负责各个业务的数据的存储。接下来说(2),现在我们已经知道,数据按业务拆到group里面去存取,那么一个group里面又应该有哪些角色呢?自然的,应该有一台主要的机器,作为group的核心,我们称它为Group Master,是的,它就是这个group的主要代表。这个group的数据,在Group Master上应该都能找到,进行读写。另外,我们还需要一些辅助角色,我们称它们为Group Slaves,这些slave机器做啥工作呢?它们负责去Group Master处拿数据,并尽量保持和它同步,并提供读服务。请注意我的用词,“尽量”,稍后将会解释。现在我们已经有了一个group的基本轮廓:

一个group提供对外的接口(废话否则怎么存取数据),group的底层可以是实际的File System,甚至是HDFS。Group Master和Group Slave可以共享同一个File System(用于不能丢数据的强一致性系统),也可以分别指向不同的File System(用于弱一致性,允许停写服务和系统宕机时丢数据的系统),但总之应认为这个”File System”是无状态,有状态的是Group Master和各个Group Slave。

下面来说一个group如何工作,同步等核心问题。首先,一个group的Group Master和Group Slave间应保持强一致性还是弱一致性(最终一致性)应取决于具体的业务需求,以我们的“山推”来说,Group Master和Group Slave并不要求保持强一致性,而弱一致性(最终一致性)即能满足要求,为什么?因为对于“山推”来讲,一个Group Master写了一个数据,而另一个Group Slave被读到一个“过期”(因为Group Master已经写,但此Group Slave还未更新此数据)的数据通常并不会带来大问题,比如,我在“山推”上发了一个推文,“关注我的人”并没有即时同步地看到我的最新推文,并没有太大影响,只要“稍后”它们能看到最新的数据即可,这就是所谓的最终一致性。但当Group Master挂掉时,写服务将中断一小段时间由其它Group Slave来顶替,稍后还要再讲这个问题。假如我们要做的系统不是山推,而是淘宝购物车,支付宝一类的,那么弱一致性(最终一致性)则很难满足要求,同时写服务挂掉也是不能忍受的,对于这样的系统,应保证“强一致性”,保证不能丢失任何数据。

接下来还是以我们的“山推“为例,看看一个group如何完成数据同步。假设,现在我有一个请求要写一个数据,由于只有Group Master能写,那么Group Master将接受这个写请求,并加入写的队列,然后Group Master将通知所有Group Slave来更新这个数据,之后这个数据才真正被写入File System。那么现在就有一个问题,是否应等所有Group Slave都更新了这个数据,才算写成功了呢?这里涉及一些NWR的概念,我们作一个取舍,即至少有一个Group Slave同步成功,才能返回写请求的成功。这是为什么呢?因为假如这时候Group Master突然挂掉了,那么我们至少可以找到一台Group Slave保持和Group Master完全同步的数据并顶替它继续工作,剩下的、其它的Group Slave将“异步”地更新这个新数据,很显然,假如现在有多个读请求过来并到达不同的Group Slave节点,它们很可能读到不一样的数据,但最终这些数据会一致,如前所述。我们做的这种取舍,叫“半同步”模式。那之前所说的强一致性系统应如何工作呢?很显然,必须得等所有Group Slave都同步完成才能返回写成功,这样Group Master挂了,没事,其它Group Slave顶上就行,不会丢失数据,但是付出的代价就是,等待同步的时间。假如我们的group是跨机房、跨地区分布的,那么等待所有Group Slave同步完成将是很大的性能挑战。所以综合考虑,除了对某些特别的系统,采用“最终一致性”和“半同步”工作的系统,是符合高并发线上应用需求的。而且,还有一个非常重要的原因,就是通常线上的请求都是读>>写,这也正是“最终一致性”符合的应用场景。

好,继续。刚才我们曾提到,如果Group Master宕机挂掉,至少可以找到一个和它保持同不的Group Slave来顶替它继续工作,其它的Group Slave则“尽量”保持和Group Master同步,如前文所述。那么这是如何做到的呢?这里涉及到“分布式选举”的概念,如Paxos协议,通过分布式选举,总能找到一个最接近Group Master的Group Slave,来顶替它,从而保证系统的可持续工作。当然,在此过程中,对于最终一致性系统,仍然会有一小段时间的写服务中断。现在继续假设,我们的“山推”已经有了一些规模,而负责“山推”推文的这个group也有了五台机器,并跨机房,跨地区分布,按照上述设计,无论哪个机房断电或机器故障,都不会影响这个group的正常工作,只是会有一些小的影响而已。

那么对于这个group,还剩2个问题,一是如何知道Group Master挂掉了呢?二是在图中我们已经看到Group Slave是可扩展的,那么新加入的Group Slave应如何去“偷”数据从而逐渐和其它节点同步呢?对于问题一,我们的方案是这样的,另外提供一个类似“心跳”的服务(由谁提供呢,后面我们将讲到的Global Master将派上用场),group内所有节点无论是Group Master还是Group Slave都不停地向这个“心跳”服务去申请一个证书,或认为是一把锁,并且这个锁是有时间的,会过期。“心跳”服务定期检查Group Master的锁和其有效性,一旦过期,如果Group Master工作正常,它将锁延期并继续工作,否则说明Group Master挂掉,由其它Group Slave竞争得到此锁(分布式选举),从而变成新的Group Master。对于问题二,则很简单,新加入的Group Slave不断地“偷”老数据,而新数据总由于Group Master通知其更新,最终与其它所有结点同步。(当然,“偷”数据所用的时间并不乐观,通常在小时级别)


我们完成了在此分布式系统中,一个group的设计。那么接下来,我们设计系统的其他部分。如前文所述,我们的业务及其数据以group为单位,显然在此系统中将存在many many的groups(别告诉我你的网站总共有一个业务,像我们的“山推”,那业务是一堆一堆地),那么由谁来管理这些groups呢?由Web过来的请求,又将如何到达指定的group,并由该group处理它的请求呢?这就是我们要讨论的问题。

我们引入了一个新的角色——Global Master,顾名思义,它是管理全局的一个节点,它主要完成如下工作:(1)管理系统全局配置,发送全局控制信息;(2)监控各个group的工作状态,提供心跳服务,若发现宕机,通知该group发起分布式选举产生新的Group Master;(3)处理Client端首次到达的请求,找出负责处理该请求的group并将此group的信息(location)返回,则来自同一个前端请求源的该类业务请求自第二次起不需要再向Global Master查询group信息(缓存机制);(4)保持和Global Slave的强一致性同步,保持自身健康状态并向全局的“心跳”服务验证自身的状态。

现在我们结合图来逐条解释上述工作,显然,这个系统的完整轮廓已经初现。

首先要明确,不管我们的系统如何“分布式”,总之会有至少一个最主要的节点,术语可称为primary node,如图所示,我们的系统中,这个节点叫Global Master,也许读过GFS + Bigtable论文的同学知道,在GFS + Bigtable里,这样的节点叫Config Master,虽然名称不一样,但所做的事情却差不多。这个主要的Global Master可认为是系统状态健康的标志之一,只要它在正常工作,那么基本可以保证整个系统的状态是基本正常的(什么?group或其他结点会不正常不工作?前面已经说过,group内会通过“分布式选举”来保证自己组内的正常工作状态,不要告诉我group内所有机器都挂掉了,那个概率我想要忽略它),假如Global Master不正常了,挂掉了,怎么办?显然,图中的Global Slave就派上用场了,在我们设计的这个“山推”系统中,至少有一个Global Slave,和Global Master保持“强一致性”的完全同步,当然,如果有不止一个Global Slave,它们也都和Global Master保持强一致性完全同步,这样有个好处,假如Global Master挂掉,不用停写服务,不用进行分布式选举,更不会读服务,随便找一个Global Slave顶替Global Master工作即可。这就是强一致性最大的好处。那么有的同学就会问,为什么我们之前的group,不能这么搞,非要搞什么最终一致性,搞什么分布式选举(Paxos协议属于既难理解又难实现的坑爹一族)呢?我告诉你,还是压力,压力。我们的系统是面向日均千万级PV以上的网站(“山推”嘛,推特是亿级PV,我们千万级也不过分吧),但系统的压力主要在哪呢?细心的同学就会发现,系统的压力并不在Global Master,更不会在Global Slave,因为他们根本不提供数据的读写服务!是的,系统的压力正是在各个group,所以group的设计才是最关键的。同时,细心的同学也发现了,由于Global Master存放的是各个group的信息和状态,而不是用户存取的数据,所以它更新较少,也不能认为读>>写,这是不成立的,所以,Global Slave和Global Master保持强一致性完全同步,正是最好的选择。所以我们的系统,一台Global Master和一台Global Slave,暂时可以满足需求了。

好,我们继续。现在已经了解Global Master的大概用途,那么,一个来自Client端的请求,如何到达真正的业务group去呢?在这里,Global Master将提供“首次查询”服务,即,新请求首次请求指定的group时,通过Global Master获得相应的group的信息,以后,Client将使用该信息直接尝试访问对应的group并提交请求,如果group信息已过期或是不正确,group将拒绝处理该请求并让Client重新向Global Master请求新的group信息。显然,我们的系统要求Client端缓存group的信息,避免多次重复地向Global Master查询group信息。这里其实又挖了许多烂坑等着我们去跳,首先,这样的工作模式满足基本的Ddos攻击条件,这得通过其他安全性措施来解决,避免group总是收到不正确的Client请求而拒绝为其服务;其次,当出现大量“首次”访问时,Global Master尽管只提供查询group信息的读服务,仍有可能不堪重负而挂掉,所以,这里仍有很大的优化空间,比较容易想到的就是采用DNS负载均衡,因为Global Master和其Global Slave保持完全同步,所以DNS负载均衡可以有效地解决“首次”查询时Global Master的压力问题;再者,这个工作模式要求Client端缓存由Global Master查询得到的group的信息,万一Client不缓存怎么办?呵呵,不用担心,Client端的API也是由我们设计的,之后才面向Web前端。

之后要说的,就是图中的“Global Heartbeat”,这又是个什么东西呢?可认为这是一个管理Global Master和Global Slave的节点,Global Master和各个Global Slave都不停向Global Heartbeat竞争成为Global Master,如果Global Master正常工作,定期更新其状态并延期其获得的锁,否则由Global Slave替换之,原理和group内的“心跳”一样,但不同的是,此处Global Master和Global Slave是强一致性的完全同步,不需要分布式选举。有同学可能又要问了,假如Global Heartbeat挂掉了呢?我只能告诉你,这个很不常见,因为它没有任何压力,而且挂掉了必须人工干预才能修复。在GFS + Bigtable里,这个Global Heartbeat叫做Lock Service。


现在接着设计我们的“山推”系统。有了前面两篇的铺垫,我们的系统现在已经有了五脏六腑,剩下的工作就是要让其羽翼丰满。那么,是时候,放出我们的“山推”系统全貌了:

前面啰嗦了半天,也许不少同学看的不明不白,好了,现在开始看图说话环节:

(1)整个系统由N台机器组合而成,其中Global Master一台,Global Slave一台到多台,两者之间保持强一致性并完全同步,可由Global Slave随时顶替Global Master工作,它们被Global Heartbeat(一台)来管理,保证有一个Global Master正常工作;Global Heartbeat由于无压力,通常认为其不能挂掉,如果它挂掉了,则必须人工干预才能恢复正常;

(2)整个系统由多个groups合成,每一个group负责相应业务的数据的存取,它们是数据节点,是真正抗压力的地方,每一个group由一个Group Master和一个到多个Group Slave构成,Group Master作为该group的主节点,提供读和写,而Group Slave则只提供读服务且保证这些Group Slave节点中,至少有一个和Group Master保持完全同步,剩余的Group Slave和Group Master能够达到最终一致,它们之间以“半同步”模式工作保证最终一致性;

(3)每一个group的健康状态由Global Master来管理,Global Master向group发送管理信息,并保证有一个Group Master正常工作,若Group Master宕机,在该group内通过分布式选举产生新的Group Master顶替原来宕机的机器继续工作,但仍然有一小段时间需要中断写服务来切换新的Group Master;

(4)每一个group的底层是实际的存储系统,File system,它们是无状态的,即,由分布式选举产生的Group Master可以在原来的File system上继续工作;

(5)Client的上端可认为是Web请求,Client在“首次”进行数据读写时,向Global Master查询相应的group信息,并将其缓存,后续将直接与相应的group进行通信;为避免大量“首次”查询冲垮Global Master,在Client与Global Master之间增加DNS负载均衡,可由Global Slave分担部分查询工作;

(6)当Client已经拥有足够的group信息时,它将直接与group通信进行工作,从而真正的压力和流量由各个group分担,并处理完成需要的工作。

好了,现在我们的“山推”系统设计完成了,但是要将它编码实现,还有很远的路要走,细枝末节的问题也会暴露更多。如果该系统用于线上计算,如有大量的Map-Reduce运行于group中,系统将会更复杂,因为此时不光考虑的数据的存储同步问题,操作也需要同步。现在来检验下我们设计的“山推”系统,主要分布式指标:

一致性:如前文所述,Global机器强一致性,Group机器最终一致性;

可用性:Global机器保证了HA(高可用性),Group机器则不保证,但满足了分区容错性;

备份Replication:Global机器采用完全同步,Group机器则是半同步模式,都可以进行横向扩展;

故障恢复:如前文所述,Global机器完全同步,故障可不受中断由slave恢复工作,但Group机器采用分布式选举和最终一致性,故障时有较短时间的写服务需要中断并切换到slave机器,但读服务可不中断。

还有其他一些指标,这里就不再多说了。还有一些细节,需要提一下,比如之前的评论中有同学提到,group中master挂时,由slave去顶替,但这样一来该group内其他所有slave需要分担之前成这新master的这个slave的压力,有可能继续挂掉而造成雪崩。针对此种情况,可采用如下做法:即在一个group内,至少还存在一个真正做“备份”用途的slave,平时不抗压力,只同步数据,这样当出现上述情况时,可由该备份slave来顶替成为新master的那个slave,从而避免雪崩效应。不过这样一来,就有新的问题,由于备份slave平时不抗压力,加入抗压力后必然产生一定的数据迁移,数据迁移也是一个较麻烦的问题。常采用的分摊压力做法如一致性Hash算法(环状Hash),可将新结点加入对整个group的影响降到较小的程度。

另外,还有一个较为棘手的问题,就是系统的日志处理,主要是系统宕机后如何恢复之前的操作日志。比较常见的方法是对日志作快照(Snapshot)和回放点(checkpoint),并采用Copy-on-write方式定期将日志作snapshot存储,当发现宕机后,找出对应的回放点并恢复之后的snapshot,但此时仍可能有新的写操作到达,并产生不一致,这里主要依靠Copy-on-write来同步。

最后再说说图中的Client部分。显然这个模块就是面向Web的接口,后面连接我们的“山推”系统,它可以包含诸多业务逻辑,最重要的,是要缓存group的信息。在Client和Web之间,还可以有诸如Nginx之类的反向代理服务器存在,做进一步性能提升,这已经超出了本文的范畴,但我们必须明白的是,一个高并发高性能的网站,对性能的要求是从起点开始的,何为起点,即用户的浏览器。

现在,让我们来看看GFS的设计:

很明显,这么牛的系统我是设计不出来的,我们的“山推”,就是在学习GFS + Bigtable的主要思想。说到这,也必须提一句,可能我文章中,名词摆的有点多了,如NWR,分布式选举,Paxos包括Copy-on-write等,有兴趣的同学可自行google了解。因为说实在的,这些概念我也没法讲透彻,只是一知半解。另外,大家可参考一些分布式项目的设计,如Cassandra,包括淘宝的Oceanbase等,以加深理解。

高并发服务端分布式系统设计概要,首发于 博客 - 伯乐在线

在系统中生成业务ID的几种方法

$
0
0

在系统中,除了使用数据库表本身的Id,如何生成各种业务Id?一下记录几种生成Id的方式:

  1. 使用数据库表记录生成的Id,以MySQL为例:

1) 首先创建一个数据库表,来记录当前的业务Id

CREATE TABLE `global_auto_number` (
  `id` varchar(32) NOT NULL,
  `version_optimized_lock` int(11) NOT NULL,
  `business_key` varchar(255) NOT NULL COMMENT 'Can use full business class name as business key',
  `current_num` bigint(22) NOT NULL COMMENT 'current number',
  PRIMARY KEY (`id`),
  UNIQUE KEY `UK_AUTO_NUMBER` (`business_key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 

 2) 获取Id的方法:

int currentCount = jdbcTemplate.update("update global_auto_number set current_num = LAST_INSERT_ID(current_num + 1) where business_key = ?", new Object[] {businessKey});

if(currentCount == 0) {
     jdbcTemplate.update("insert into global_auto_number (id, version_optimized_lock, business_key, current_num) values (?, 1, ?, 0)", new Object[] {UUIDGenerator.generateUUID(),businessKey});
     jdbcTemplate.update("update global_auto_number set current_num = LAST_INSERT_ID(current_num + 1) where business_key = ?", new Object[] {businessKey});
}
return jdbcTemplate.queryForLong("select LAST_INSERT_ID()");

 3) 按照业务逻辑格式化获取的Id。例如:

String.format("SEQ%09d", 123)

 4) 注意事务需要用REQUIRES_NEW,否则在并发环境下会出现大量乐观锁问题。

 

2. 使用随机数来生成随机的Id,例如使用同一个Id来追踪后台响应用户操作的各种log

   public static String generateRandomId() {
      byte[] bytes = new byte[10];
      try {
         SecureRandom.getInstance("SHA1PRNG").nextBytes(bytes);
      }
      catch (NoSuchAlgorithmException e) {
         throw new RuntimeException(e);
      }
      return toHexString(bytes);
   }
   final protected static char[] hexArray = "0123456789ABCDEF".toCharArray();
   public static String toHexString(byte[] bytes) {
       char[] hexChars = new char[bytes.length * 2];
       for ( int j = 0; j < bytes.length; j++ ) {
           int v = bytes[j] & 0xFF;
           hexChars[j * 2] = hexArray[v >>> 4];
           hexChars[j * 2 + 1] = hexArray[v & 0x0F];
       }
       return new String(hexChars);
   }

 

 

 

 

 

 

 



已有 0人发表留言,猛击->> 这里<<-参与讨论


ITeye推荐



log4j与logback自定义文件存放目录方法

$
0
0

       为了方便日志的管理,我们在集群之间通过网络挂载的方式创建了一个共享目录即在所有的服务器上均可以访问此磁盘目录。因此我们在写日志时需要根据集群的环境动态的设定日志的存储路径。

       我们的工程日志的记录采用了两种方式log4j和sl4f+logback

一、log4j自定义路径

       1、创建类LogbackCustomName让其继承ServletContextListener。

   public static final String log4jdirkey = "log4jdir";

 @Override
 public void contextDestroyed(ServletContextEvent log4jdirkey) {

  System.getProperties().remove(log4jdirkey);

 }

 @Override
 public void contextInitialized(ServletContextEvent servletcontextevent) {
  InetAddress netAddress = getInetAddress();

//获取主机名 此方法也可以获取主机IP但是只能在windows中使用

  String log4jdir = getHostName(netAddress);

  System.setProperty(log4jdirkey, log4jdir);
 }

 public static InetAddress getInetAddress() {

  try {
   return InetAddress.getLocalHost();
  } catch (UnknownHostException e) {
   e.printStackTrace();
  }
  return null;

 }

 public static String getHostName(InetAddress netAddress) {
  if (null == netAddress) {
   return null;
  }
  String ip = netAddress.getHostName();
  return ip;
 }
2、修改web.xml文件

       在web.xml中增加监听

 <listener>
  <listener-class>XXX.XXXX.LogbackCustomName</listener-class>
 </listener>

      这一段一定要放在Spring的监听之前,否则不会生效。

 配置完成后在日志写入路径中加上${log4jdir}即可。

 二、logback自定义路径

1、首先创建类LogbackCustomName继承logback中的PropertyDefinerBase

  @Override
 public String getPropertyValue() {
  String info;
  InetAddress netAddress = getInetAddress();

//获取主机名 linux多网卡无法根据环境指定具体网卡,此方法只能在windows下使用
  info = getHostName(netAddress); 
  return info;

 }

 public static InetAddress getInetAddress() {

  try {
   return InetAddress.getLocalHost();
  } catch (UnknownHostException e) {
   e.printStackTrace();
  }
  return null;

 }

 public static String getHostName(InetAddress netAddress) {
  if (null == netAddress) {
   return null;
  }
  String ip = netAddress.getHostName();
  return ip;
 }

二、修改logback.xml配置文件,增加自定义变量

 <define  name="HostName" class="XXX.XXX..LogbackCustomName" /> 

然后在定义路径时在路径名上加上${HostName}即可。

 



已有 0人发表留言,猛击->> 这里<<-参与讨论


ITeye推荐



【译】使用 AngularJS 和 Electron 构建桌面应用

$
0
0

原文: Creating Desktop Applications With AngularJS and GitHub Electron

angular-electron-cover.png

GitHub 的 Electron框架(以前叫做 Atom Shell)允许你使用 HTML, CSS 和 JavaScript 编写跨平台的桌面应用。它是 io.js运行时的衍生,专注于桌面应用而不是 web 服务端。

Electron 丰富的原生 API 使我们能够在页面中直接使用 JavaScript 获取原生的内容。

这个教程向我们展示了如何使用 Angular 和 Electron 构建一个桌面应用。下面是本教程的所有步骤:

  1. 创建一个简单的 Electron 应用

  2. 使用 Visual Studio Code 编辑器管理我们的项目和任务

  3. 使用 Electron 开发(原文为 Integrate)一个 Angular 顾客管理应用(Angular Customer Manager App)

  4. 使用 Gulp 任务构建我们的应用,并生成安装包

创建你的 Electron 应用

起初,如果你的系统中还没有安装 Node,你需要先安装它。我们应用的结构如下所示:

project-structure.png

这个项目中有两个 package.json 文件。

  • 开发使用
    项目根目录下的 package.json 包含你的配置,开发环境的依赖和构建脚本。这些依赖和 package.json 文件不会被打包到生产环境构建中。

  • 应用使用
    app 目录下的 package.json 是你应用的清单文件。因此每当在你需要为你项目安装 npm 依赖的时候,你应该依照这个 package.json 来进行安装。

package.json 的格式和 Node 模块中的完全一致。你应用的启动脚本(的路径)需要在 app/package.json中的 main属性中指定。

app/package.json看起来是这样的:

{
  name: "AngularElectron", 
  version: "0.0.0", 
  main: "main.js" 
}

过执行 npm init命令分别创建这两个 package.json文件,也可以手动创建它们。通过在命令提示行里键入以下命令来安装项目打包必要的 npm 依赖:

npm install --save-dev electron-prebuilt fs-jetpack asar rcedit Q

创建启动脚本

app/main.js是我们应用的入口。它负责创建主窗口和处理系统事件。 main.js应该如下所示:

// app/main.js

// 应用的控制模块
var app = require('app'); 

// 创建原生浏览器窗口的模块
var BrowserWindow = require('browser-window');
var mainWindow = null;

// 当所有窗口都关闭的时候退出应用
app.on('window-all-closed', function () {
  if (process.platform != 'darwin') {
    app.quit();
  }
});

// 当 Electron 结束的时候,这个方法将会生效
// 初始化并准备创建浏览器窗口
app.on('ready', function () {

  // 创建浏览器窗口.
  mainWindow = new BrowserWindow({ width: 800, height: 600 });

  // 载入应用的 index.html
  mainWindow.loadUrl('file://' + __dirname + '/index.html');

  // 打开开发工具
  // mainWindow.openDevTools();
  // 窗口关闭时触发
  mainWindow.on('closed', function () {

    // 想要取消窗口对象的引用,如果你的应用支持多窗口,
    // 通常你需要将所有的窗口对象存储到一个数组中,
    // 在这个时候你应该删除相应的元素
    mainWindow = null;
  });
  
});

通过 DOM 访问原生

正如我上面提到的那样,Electron 使你能够直接在 web 页面中访问本地 npm 模块和原生 API。你可以这样创建 app/index.html文件:

<html><body> <h1>Hello World!</h1>
  We are using Electron <script>  document.write(process.versions['electron']) </script><script> document.write(process.platform) </script><script type="text/javascript"> 
     var fs = require('fs');
     var file = fs.readFileSync('app/package.json'); 
     document.write(file); </script></body> </html>

app/index.html是一个简单的 HTML 页面。在这里,它通过使用 Node’s fs (file system) 模块来读取 package.json文件并将其内容写入到 document body 中。

运行应用

一旦你创建好了项目结构、 app/index.htmlapp/main.jsapp/package.json,你很可能想要尝试去运行初始的 Electron 应用来测试并确保它正常工作。

如果你已经在系统中全局安装了 electron-prebuilt,就可以通过下面的命令启动应用:

electron app

在这里, electron是运行 electron shell 的命令, app是我们应用的目录名。如果你不想将 Election 安装到你全局的 npm 模块中,可以在命令提示行中通过下面命令使用本地 npm_modules文件夹下的 electron 来启动应用。

"node_modules/.bin/electron" "./app" 

尽管你可以这样来运行应用,但是我还是建议你在 gulpfile.js中创建一个 gulp task,这样你就可以将你的任务和 Visual Studio Code 编辑器相结合,我们会在下一部分展示。

// 获取依赖
var gulp        = require('gulp'), 
  childProcess  = require('child_process'), 
  electron      = require('electron-prebuilt');

// 创建 gulp 任务
gulp.task('run', function () { 
  childProcess.spawn(electron, ['./app'], { stdio: 'inherit' }); 
});

运行你的 gulp 任务: gulp run。我们的应用看起来会是这个样子:

electron-app

配置 Visual Studio Code 开发环境

Visual Studio Code 是微软的一款跨平台代码编辑器。VS Code 是基于 Electron 和 微软自身的 Monaco Code Editor 开发的。你可以在 这里下载到 Visual Studio Code。

在 VS Code 中打开你的 electron 应用。

open-application.png

配置 Visual Studio Code Task Runner

有很多自动化的工具,像构建、打包和测试等。我们大多从命令行中运行这些工具。VS Code task runner 使你能够将你自定义的任务集成到项目中。你可以在你的项目中直接运行 grunt,、gulp,、MsBuild 或者其他任务,这并不需要移步到命令行。

VS Code 能够自动检测你的 grunt 和 gulp 任务。按下 ctrl + shift + p然后键入 Run Task敲击回车便可。

run-task.png

你将从 gulpfile.jsgruntfile.js文件中获取所有有效的任务。

注意:你需要确保 gulpfile.js文件存在于你应用的根目录下。

run-task-gulp.png

ctrl + shift + b会从你任务执行器(task runner)中执行 build任务。你可以使用 task.json文件来覆盖任务集成。按下 ctrl + shift + p然后键入 Configure Task敲击回车。这将会在你项目中创建一个 .setting的文件夹和 task.json文件。要是你不止想要执行简单的任务,你需要在 task.json中进行配置。例如你或许想要通过按下 Ctrl + Shift + B来运行应用,你可以这样编辑 task.json文件:

{ "version": "0.1.0", "command": "gulp", "isShellCommand": true, "args": [ "--no-color" ], "tasks": [ 
    { "taskName": "run", "args": [], "isBuildCommand": true 
    } 
  ] 
} 

根部分声明命令为 gulp。你可以在 tasks部分写入你想要的更多任务。将一个任务的 isBuildCommand设置为 true 意味着它和 Ctrl + Shift + B进行了绑定。目前 VS Code 只支持一个顶级任务。

现在,如果你按下 Ctrl + Shift + Bgulp run将会被执行。

你可以在 这里阅读到更多关于 visual studio code 任务的信息。

调试 Electron 应用

打开调试面板点击配置按钮就会在 .settings文件夹内创建一个 launch.json文件,包含了调试的配置。

debug.png

我们不需要启动 app.js 的配置,所以移除它。

现在,你的 launch.json应该如下所示:

{ "version": "0.1.0", 
  // 配置列表。添加新的配置或更改已存在的配置。
  // 仅支持 "node" 和 "mono",可以改变 "type" 来进行切换。
  "configurations": [
    { "name": "Attach", "type": "node", 
      // TCP/IP 地址. 默认是 "localhost""address": "localhost", 
      // 建立连接的端口.
      "port": 5858, "sourceMaps": false 
     } 
   ] 
}

按照下面所示更改之前创建的 gulp run任务,这样我们的 electron 将会采用调试模式运行,5858 端口也会被监听。

gulp.task('run', function () { 
  childProcess.spawn(electron, ['--debug=5858','./app'], { stdio: 'inherit' }); 
}); 

在调试面板中选择 “Attach” 配置项,点击开始(run)或者按下 F5。稍等片刻后你应该就能在上部看到调试命令面板。

debug-star.png

创建 AngularJS 应用

第一次接触 AngularJS?浏览 官方网站或一些 Scotch Angular 教程

这一部分会讲解如何使用 AngularJS 和 MySQL 数据库创建一个顾客管理(Customer Manager)应用。这个应用的目的不是为了强调 AngularJS 的核心概念,而是展示如何在 GiHub 的 Electron 中同时使用 AngularJS 和 NodeJS 以及 MySQL 。

我们的顾客管理应用正如下面这样简单:

  • 顾客列表

  • 添加新顾客

  • 选择删除一个顾客

  • 搜索指定的顾客

项目结构

我们的应用在 app文件夹下,目录结构如下所示:

angular-project-structure.png

主页是 app/index.html文件。 app/scripts文件夹包含所有用在该应用中的关键脚本和视图。有许多方法可以用来组织应用的文件。

这里我更喜欢按照功能来组织脚本文件。每个功能都有它自己的文件夹,文件夹中有模板和控制器。获取更多关于目录结构的信息,可以阅读 AngularJS 最佳实践: 目录结构

在开始 AngularJS 应用之前,我们将使用 bower安装客户端方面的依赖。如果你还没有 Bower先要安装它。在命令提示行中将当前工作目录切换至你应用的根目录,然后依照下面的命令安装依赖。

bower install angular angular-route angular-material --save 

设置数据库

在这个例子中,我将使用一个名字为 customer-manager的数据库和一张名字为 customers的表。下面是数据库的导出文件,你可以依照这个快速开始。


CREATE TABLE `customer_manager`.`customers` ( 
  `customer_id` INT NOT NULL AUTO_INCREMENT, 
  `name` VARCHAR(45) NOT NULL, 
  `address` VARCHAR(450) NULL, 
  `city` VARCHAR(45) NULL, 
  `country` VARCHAR(45) NULL, 
  `phone` VARCHAR(45) NULL, 
  `remarks` VARCHAR(500) NULL, PRIMARY KEY (`customer_id`) 
);

创建一个 Angular Service 和 MySQL 进行交互

一旦你的数据库和表都准备好了,就可以开始创建一个 AngularJS service 来直接从数据库中获取数据。使用 node-mysql这个 npm 模块使 service 连接数据库——一个使用 JavaScript 为 NodeJs 编写的 MySQL 驱动。在你 Angular 应用的 app/目录下安装 node-mysql模块。

注意:我们将 node-mysql 模块安装到 app 目录下而不是应用的根目录,是因为我们需要在最终的 distribution 中包含这个模块。

在命令提示行中切换工作目录至 app文件夹然后按照下面所示安装模块:

npm install --save mysql 

我们的 angular service —— app/scripts/customer/customerService.js如下所示:

(function () {'use strict';
    var mysql = require('mysql');

    // 创建 MySql 数据库连接
    var connection = mysql.createConnection({
        host: "localhost",
        user: "root",
        password: "password",
        database: "customer_manager"
    });
    angular.module('app')
        .service('customerService', ['$q', CustomerService]);

    function CustomerService($q) {
        return {
            getCustomers: getCustomers,
            getById: getCustomerById,
            getByName: getCustomerByName,
            create: createCustomer,
            destroy: deleteCustomer,
            update: updateCustomer
        };

        function getCustomers() {
            var deferred = $q.defer();
            var query = "SELECT * FROM customers";
            connection.query(query, function (err, rows) {
                if (err) deferred.reject(err);
                deferred.resolve(rows);
            });
            return deferred.promise;
        }   

        function getCustomerById(id) {
            var deferred = $q.defer();
            var query = "SELECT * FROM customers WHERE customer_id = ?";
            connection.query(query, [id], function (err, rows) {
                if (err) deferred.reject(err);
                deferred.resolve(rows);
            });
            return deferred.promise;
        }     

        function getCustomerByName(name) {
            var deferred = $q.defer();
            var query = "SELECT * FROM customers WHERE name LIKE  '" + name + "%'";
            connection.query(query, [name], function (err, rows) {
                if (err) deferred.reject(err);
                deferred.resolve(rows);
            });
            return deferred.promise;
        }

        function createCustomer(customer) {
            var deferred = $q.defer();
            var query = "INSERT INTO customers SET ?";
            connection.query(query, customer, function (err, res) 
                if (err) deferred.reject(err);
                deferred.resolve(res.insertId);
            });
            return deferred.promise;
        }

        function deleteCustomer(id) {
            var deferred = $q.defer();
            var query = "DELETE FROM customers WHERE customer_id = ?";
            connection.query(query, [id], function (err, res) {
                if (err) deferred.reject(err);
                deferred.resolve(res.affectedRows);
            });
            return deferred.promise;
        }     

        function updateCustomer(customer) {
            var deferred = $q.defer();
            var query = "UPDATE customers SET name = ? WHERE customer_id = ?";
            connection.query(query, [customer.name, customer.customer_id], function (err, res) {
                if (err) deferred.reject(err);
                deferred.resolve(res);
            });
            return deferred.promise;
        }
    }
})();

customerService是一个简单的自定义 angular service,它提供了对表 customers的基础 CRUD 操作。直接在 service 中使用了 node 模块 mysql。如果你已经拥有了一个远程的数据服务,你也可以使用它来替代之。

控制器 & 模板

app/scripts/customer/customerController中的 customerController如下所示:

(function () {'use strict';
    angular.module('app')
        .controller('customerController', ['customerService', '$q', '$mdDialog', CustomerController]);
    function CustomerController(customerService, $q, $mdDialog) {
        var self = this; 

        self.selected = null;
        self.customers = [];
        self.selectedIndex = 0;
        self.filterText = null;
        self.selectCustomer = selectCustomer;
        self.deleteCustomer = deleteCustomer;
        self.saveCustomer = saveCustomer;
        self.createCustomer = createCustomer;
        self.filter = filterCustomer;   

        // 载入初始数据
        getAllCustomers();

        //----------------------
        // 内部方法
        //----------------------

        function selectCustomer(customer, index) {
            self.selected = angular.isNumber(customer) ? self.customers[customer] : customer;
            self.selectedIndex = angular.isNumber(customer) ? customer: index;
        }
        
        function deleteCustomer($event) {
            var confirm = $mdDialog.confirm()
                                   .title('Are you sure?')
                                   .content('Are you sure want to delete this customer?')
                                   .ok('Yes')
                                   .cancel('No')
                                   .targetEvent($event);

            $mdDialog.show(confirm).then(function () {
                customerService.destroy(self.selected.customer_id).then(function (affectedRows) {
                    self.customers.splice(self.selectedIndex, 1);
                });
            }, function () { });
        }

        function saveCustomer($event) {
            if (self.selected != null && self.selected.customer_id != null) {
                customerService.update(self.selected).then(function (affectedRows) {
                    $mdDialog.show(
                        $mdDialog
                            .alert()
                            .clickOutsideToClose(true)
                            .title('Success')
                            .content('Data Updated Successfully!')
                            .ok('Ok')
                            .targetEvent($event)
                    );
                });
            }
            else {
                //self.selected.customer_id = new Date().getSeconds();
                customerService.create(self.selected).then(function (affectedRows) {
                    $mdDialog.show(
                        $mdDialog
                            .alert()
                            .clickOutsideToClose(true)
                            .title('Success')
                            .content('Data Added Successfully!')
                            .ok('Ok')
                            .targetEvent($event)
                    );
                });
            }
        }    

        function createCustomer() {
            self.selected = {};
            self.selectedIndex = null;
        }      

        function getAllCustomers() {
            customerService.getCustomers().then(function (customers) {
                self.customers = [].concat(customers);
                self.selected = customers[0];
            });
        }
       
        function filterCustomer() {
            if (self.filterText == null || self.filterText == "") {
                getAllCustomers();
            }
            else {
                customerService.getByName(self.filterText).then(function (customers) {
                    self.customers = [].concat(customers);
                    self.selected = customers[0];
                });
            }
        }
    }

})();

我们的顾客模板( app/scripts/customer/customer.html)使用了 angular material 组件来构建 UI,如下所示:

<div style="width:100%" layout="row"><md-sidenav class="site-sidenav md-sidenav-left md-whiteframe-z2"
                md-component-id="left"
                md-is-locked-open="$mdMedia('gt-sm')"><md-toolbar layout="row" class="md-whiteframe-z1"><h1>Customers</h1></md-toolbar><md-input-container style="margin-bottom:0"><label>Customer Name</label><input required name="customerName" ng-model="_ctrl.filterText" ng-change="_ctrl.filter()"></md-input-container><md-list><md-list-item ng-repeat="it in _ctrl.customers"><md-button ng-click="_ctrl.selectCustomer(it, $index)" ng-class="{'selected' : it === _ctrl.selected }">
                    {{it.name}}</md-button></md-list-item></md-list></md-sidenav><div flex layout="column" tabIndex="-1" role="main" class="md-whiteframe-z2"><md-toolbar layout="row" class="md-whiteframe-z1"><md-button class="menu" hide-gt-sm ng-click="ul.toggleList()" aria-label="Show User List"><md-icon md-svg-icon="menu"></md-icon></md-button><h1>{{ _ctrl.selected.name }}</h1></md-toolbar><md-content flex id="content"><div layout="column" style="width:50%"><br /><md-content layout-padding class="autoScroll"><md-input-container><label>Name</label><input ng-model="_ctrl.selected.name" type="text"></md-input-container><md-input-container md-no-float><label>Email</label><input ng-model="_ctrl.selected.email" type="text"></md-input-container><md-input-container><label>Address</label><input ng-model="_ctrl.selected.address"  ng-required="true"></md-input-container><md-input-container md-no-float><label>City</label><input ng-model="_ctrl.selected.city" type="text" ></md-input-container><md-input-container md-no-float><label>Phone</label><input ng-model="_ctrl.selected.phone" type="text"></md-input-container></md-content><section layout="row" layout-sm="column" layout-align="center center" layout-wrap><md-button class="md-raised md-info" ng-click="_ctrl.createCustomer()">Add</md-button><md-button class="md-raised md-primary" ng-click="_ctrl.saveCustomer()">Save</md-button><md-button class="md-raised md-danger" ng-click="_ctrl.cancelEdit()">Cancel</md-button><md-button class="md-raised md-warn" ng-click="_ctrl.deleteCustomer()">Delete</md-button></section></div></md-content></div></div>

app.js 包含模块初始化脚本和应用的路由配置,如下所示:

(function () {'use strict';
    var _templateBase = './scripts';
    angular.module('app', ['ngRoute','ngMaterial','ngAnimate'
    ])
    .config(['$routeProvider', function ($routeProvider) {
            $routeProvider.when('/', {
                templateUrl: _templateBase + '/customer/customer.html' ,
                controller: 'customerController',
                controllerAs: '_ctrl'
            });
            $routeProvider.otherwise({ redirectTo: '/' });
        }
    ]);

})();

最后是我们的首页 app/index.html

<html lang="en" ng-app="app"><title>Customer Manager</title><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"gt;<meta name="description" content=""><meta name="viewport" content="initial-scale=1, maximum-scale=1, user-scalable=no" /><!-- build:css assets/css/app.css --><link rel="stylesheet" href="../bower_components/angular-material/angular-material.css" /><link rel="stylesheet" href="assets/css/style.css" /><!-- endbuild --><body><ng-view></ng-view><!-- build:js scripts/vendor.js --><script src="../bower_components/angular/angular.js"></script><script src="../bower_components/angular-route/angular-route.js"></script><script src="../bower_components/angular-animate/angular-animate.js"></script><script src="../bower_components/angular-aria/angular-aria.js"></script><script src="../bower_components/angular-material/angular-material.js"></script><!-- endbuild --><!-- build:app scripts/app.js --><script src="./scripts/app.js"></script><script src="./scripts/customer/customerService.js"></script><script src="./scripts/customer/customerController.js"></script><!-- endbuild --></body></html>

如果你已经如上面那样配置过 VS Code task runner 的话,使用 gulp run命令或者按下 Ctrl + Shif + B来启动你的应用。

angular-app.png

构建 AngularJS 应用

为了构建我们的 Angular 应用,需要安装 gulp-uglify, gulp-minify-cssgulp-usemin依赖包。

npm install --save gulp-uglify gulp-minify-css gulp-usemin

打开你的 gulpfile.js并且引入必要的模块。

  var childProcess = require('child_process'); 
  var electron     = require('electron-prebuilt'); 
  var gulp         = require('gulp'); 
  var jetpack      = require('fs-jetpack'); 
  var usemin       = require('gulp-usemin'); 
  var uglify       = require('gulp-uglify');

  var projectDir = jetpack; 
  var srcDir     = projectDir.cwd('./app'); 
  var destDir    = projectDir.cwd('./build');

如果构建目录已经存在的话,清理一下它。

gulp.task('clean', function (callback) { 
  return destDir.dirAsync('.', { empty: true }); 
});

复制文件到构建目录。我们并不需要使用复制功能来复制 angular 应用的代码,在下一部分中 usemin将会为我们做这件事请:

gulp.task('copy', ['clean'], function () { 
    return projectDir.copyAsync('app', destDir.path(), { 
        overwrite: true, matching: [ './node_modules/**/*', '*.html', '*.css', 'main.js', 'package.json' 
       ] 
    }); 
});

我们的构建任务将使用 gulp.src() 获取 app/index.html 然后传递给 usemin。然后它会将输出写入到构建目录并且把 index.html 中的引用用优化版代码替换掉 。

注意: 千万不要忘记在 app/index.html 像这样定义 usemin 块:

<!-- build:js scripts/vendor.js --><script src="../bower_components/angular/angular.js"></script><script src="../bower_components/angular-route/angular-route.js"></script><script src="../bower_components/angular-animate/angular-animate.js"></script><script src="../bower_components/angular-aria/angular-aria.js"></script><script src="../bower_components/angular-material/angular-material.js"></script><!-- endbuild --><!-- build:app scripts/app.js --><script src="./scripts/app.js"></script><script src="./scripts/customer/customerService.js"></script><script src="./scripts/customer/customerController.js"></script><!-- endbuild -->

构建任务如下所示:

gulp.task('build', ['copy'], function () { 
  return gulp.src('./app/index.html') 
    .pipe(usemin({ 
      js: [uglify()] 
    })) 
    .pipe(gulp.dest('build/')); 
});

为发行(distribution)做准备

在这一部分我们将把 Electron 应用打包至生产环境。在根目录创建构建脚本 build.windows.js。这个脚本用于 Windows 上。对于其他平台来说,你应该创建那个平台特定的脚本并且根据平台来运行。

可以在 node_modules/electron-prebuilt/dist目录中找到一个典型的 electron distribution。这里是构建 electron 应用的步骤:

  • 我们首要的任务是复制 electron distribution 到我们的 dist目录。

  • 每一个 electron distribution 都包含一个默认的应用在 dist/resources/default_app中 。我们需要用我们最终构建的应用来替换它。

  • 为了保护我们的应用源码和资源,你可以选择将你的应用打包成一个 asar 归档,这会改变一点你的源码。一个 asar 归档是一个简单的类似 tar 的格式,它会将你所有的文件拼接成单个文件,Electron 可以在不解压整个文件的情况下从中读取任意文件。

注意:这一部分描述的是 windows 平台下的打包。其他平台中的步骤是一样的,只是路径和使用的文件不一样而已。你可以在 github 中获取 OSx 和 linux 的完整构建脚本。

安装构建 electron 必要的依赖: npm install --save q asar fs-jetpack recedit

接下来,初始化我们的构建脚本,如下所示:

var Q = require('q'); 
var childProcess = require('child_process'); 
var asar = require('asar'); 
var jetpack = require('fs-jetpack');
var projectDir;
var buildDir; 
var manifest; 
var appDir;

function init() { 
    // 项目路径是应用的根目录
    projectDir = jetpack; 
    // 构建目录是最终应用被构建后放置的目录
    buildDir = projectDir.dir('./dist', { empty: true }); 
    // angular 应用目录
    appDir = projectDir.dir('./build'); 
    // angular 应用的 package.json 文件
    manifest = appDir.read('./package.json', 'json'); 
    return Q(); 
} 

这里我们使用 fs-jetpack node 模块进行文件操作。它提供了更灵活的文件操作。

复制 Electron Distribution

electron-prebuilt/dist复制默认的 electron distribution 到我们的 dist 目录

function copyElectron() { 
     return projectDir.copyAsync('./node_modules/electron-prebuilt/dist', buildDir.path(), { overwrite: true }); 
} 

清理默认应用

你可以在 resources/default_app文件夹内找到一个默认的 HTML 应用。我们需要用我们自己的 angular 应用来替换它。按照下面所示移除它:

注意:这里的路径是针对 windows 平台的。对于其他平台过程是一致的,只是路径不一样而已。在 OSX 中路径应该是 Contents/Resources/default_app

function cleanupRuntime() { 
     return buildDir.removeAsync('resources/default_app'); 
}

创建 asar 包

function createAsar() { 
     var deferred = Q.defer(); 
     asar.createPackage(appDir.path(), buildDir.path('resources/app.asar'), function () { 
         deferred.resolve(); 
     }); 
     return deferred.promise; 
}

这将会把你 angular 应用的所有文件打包到一个 asar 包文件里。你可以在 dist/resources/目录中找到 asar 文件。

替换为自己的应用资源

下一步是将默认的 electron icon 替换成你自己的,更新产品的信息然后重命名应用。

function updateResources() {
    var deferred = Q.defer();

    // 将你的 icon 从 resource 文件夹复制到构建文件夹下
    projectDir.copy('resources/windows/icon.ico', buildDir.path('icon.ico'));

    // 将 Electron icon 替换成你自己的
    var rcedit = require('rcedit');
    rcedit(buildDir.path('electron.exe'), {'icon': projectDir.path('resources/windows/icon.ico'),'version-string': {'ProductName': manifest.name,'FileDescription': manifest.description,
        }
    }, function (err) {
        if (!err) {
            deferred.resolve();
        }
    });
    return deferred.promise;
}
// 重命名 electron exe 
function rename() {
    return buildDir.renameAsync('electron.exe', manifest.name + '.exe');
}

创建原生安装包

你可以使用 wix 或 NSIS 创建 windows 安装包。这里我们尽可能使用更小更灵活的 NSIS,它很适合网络应用。使用 NSIS 可以创建支持应用安装时需要的任何事情的安装包。

在 resources/windows/installer.nsis 中创建 NSIS 脚本

!include LogicLib.nsh
    !include nsDialogs.nsh

    ; --------------------------------
    ; Variables
    ; --------------------------------

    !define dest "{{dest}}"
    !define src "{{src}}"
    !define name "{{name}}"
    !define productName "{{productName}}"
    !define version "{{version}}"
    !define icon "{{icon}}"
    !define banner "{{banner}}"

    !define exec "{{productName}}.exe"

    !define regkey "Software\${productName}"
    !define uninstkey "Software\Microsoft\Windows\CurrentVersion\Uninstall\${productName}"

    !define uninstaller "uninstall.exe"

    ; --------------------------------
    ; Installation
    ; --------------------------------

    SetCompressor lzma

    Name "${productName}"
    Icon "${icon}"
    OutFile "${dest}"
    InstallDir "$PROGRAMFILES\${productName}"
    InstallDirRegKey HKLM "${regkey}" ""

    CRCCheck on
    SilentInstall normal

    XPStyle on
    ShowInstDetails nevershow
    AutoCloseWindow false
    WindowIcon off

    Caption "${productName} Setup"
    ; Don't add sub-captions to title bar
    SubCaption 3 " "
    SubCaption 4 " "

    Page custom welcome
    Page instfiles

    Var Image
    Var ImageHandle

    Function .onInit

        ; Extract banner image for welcome page
        InitPluginsDir
        ReserveFile "${banner}"
        File /oname=$PLUGINSDIR\banner.bmp "${banner}"

    FunctionEnd

    ; Custom welcome page
    Function welcome

        nsDialogs::Create 1018

        ${NSD_CreateLabel} 185 1u 210 100% "Welcome to ${productName} version ${version} installer.$\r$\n$\r$\nClick install to begin."

        ${NSD_CreateBitmap} 0 0 170 210 ""
        Pop $Image
        ${NSD_SetImage} $Image $PLUGINSDIR\banner.bmp $ImageHandle

        nsDialogs::Show

        ${NSD_FreeImage} $ImageHandle

    FunctionEnd

    ; Installation declarations
    Section "Install"

        WriteRegStr HKLM "${regkey}" "Install_Dir" "$INSTDIR"
        WriteRegStr HKLM "${uninstkey}" "DisplayName" "${productName}"
        WriteRegStr HKLM "${uninstkey}" "DisplayIcon" '"$INSTDIR\icon.ico"'
        WriteRegStr HKLM "${uninstkey}" "UninstallString" '"$INSTDIR\${uninstaller}"'

        ; Remove all application files copied by previous installation
        RMDir /r "$INSTDIR"

        SetOutPath $INSTDIR

        ; Include all files from /build directory
        File /r "${src}\*"

        ; Create start menu shortcut
        CreateShortCut "$SMPROGRAMS\${productName}.lnk" "$INSTDIR\${exec}" "" "$INSTDIR\icon.ico"

        WriteUninstaller "${uninstaller}"

    SectionEnd

    ; --------------------------------
    ; Uninstaller
    ; --------------------------------

    ShowUninstDetails nevershow

    UninstallCaption "Uninstall ${productName}"
    UninstallText "Don't like ${productName} anymore? Hit uninstall button."
    UninstallIcon "${icon}"

    UninstPage custom un.confirm un.confirmOnLeave
    UninstPage instfiles

    Var RemoveAppDataCheckbox
    Var RemoveAppDataCheckbox_State

    ; Custom uninstall confirm page
    Function un.confirm

        nsDialogs::Create 1018

        ${NSD_CreateLabel} 1u 1u 100% 24u "If you really want to remove ${productName} from your computer press uninstall button."

        ${NSD_CreateCheckbox} 1u 35u 100% 10u "Remove also my ${productName} personal data"
        Pop $RemoveAppDataCheckbox

        nsDialogs::Show

    FunctionEnd

    Function un.confirmOnLeave

        ; Save checkbox state on page leave
        ${NSD_GetState} $RemoveAppDataCheckbox $RemoveAppDataCheckbox_State

    FunctionEnd

    ; Uninstall declarations
    Section "Uninstall"

        DeleteRegKey HKLM "${uninstkey}"
        DeleteRegKey HKLM "${regkey}"

        Delete "$SMPROGRAMS\${productName}.lnk"

        ; Remove whole directory from Program Files
        RMDir /r "$INSTDIR"

        ; Remove also appData directory generated by your app if user checked this option
        ${If} $RemoveAppDataCheckbox_State == ${BST_CHECKED}
            RMDir /r "$LOCALAPPDATA\${name}"
        ${EndIf}

    SectionEnd

build.windows.js文件中创建一个叫做 createInstaller的函数,如下所示:

function createInstaller() {
    var deferred = Q.defer();

    function replace(str, patterns) {
        Object.keys(patterns).forEach(function (pattern) {
            console.log(pattern)
              var matcher = new RegExp('{{' + pattern + '}}', 'g');
            str = str.replace(matcher, patterns[pattern]);
        });
        return str;
    }

    var installScript = projectDir.read('resources/windows/installer.nsi');

    installScript = replace(installScript, {
        name: manifest.name,
        productName: manifest.name,
        version: manifest.version,
        src: buildDir.path(),
        dest: projectDir.path(),
        icon: buildDir.path('icon.ico'),
        setupIcon: buildDir.path('icon.ico'),
        banner: projectDir.path('resources/windows/banner.bmp'),
    });
    buildDir.write('installer.nsi', installScript);

    var nsis = childProcess.spawn('makensis', [buildDir.path('installer.nsi')], {
        stdio: 'inherit'
    });

    nsis.on('error', function (err) {
        if (err.message === 'spawn makensis ENOENT') {
            throw "Can't find NSIS. Are you sure you've installed it and"
            + " added to PATH environment variable?";
        } else {
            throw err;
        }
    });

    nsis.on('close', function () {
        deferred.resolve();
    });

    return deferred.promise;

}

你应该安装了 NSIS,并且确保它在你的路径中是可用的。 creaeInstaller函数会读取安装包脚本并且依照 NSIS 运行时使用 makensis命令来执行。

将他们组合到一起

创建一个函数把所有的片段放在一起,为了使 gulp 任务可以获取到然后输出它:

function build() { 
    return init()
            .then(copyElectron) 
            .then(cleanupRuntime) 
            .then(createAsar) 
            .then(updateResources) 
            .then(rename) 
            .then(createInstaller); 
}
module.exports = { build: build };

接着,在 gulpfile.js中创建 gulp 任务来执行这个构建脚本:

var release_windows = require('./build.windows'); 
var os = require('os'); 
gulp.task('build-electron', ['build'], function () { 
    switch (os.platform()) { 
        case 'darwin': 
        // 执行 build.osx.js 
        break; 
        case 'linux': 
        //执行 build.linux.js 
        break; 
        case 'win32': 
        return release_windows.build(); 
    } 
}); 

运行下面命令,你应该就会得到最终的产品:

gulp build-electron

你最终的 electron 应用应该在 dist目录中,并且目录结构应该和下面是相似的:

总结

Electron 不仅仅是一个支持打包 web 应用成为桌面应用的原生 web view。它现在包含 app 的自动升级、Windows 安装包、崩溃报告、通知和一些其它有用的原生 app 功能——所有的这些都通过 JavaScript API 调用。

到目前为止,很大范围的应用使用 electron 创建,包括聊天应用、数据库管理器、地图设计器、协作设计工具和手机原型等。

下面是 Github Electron 的一些有用的资源:

Viewing all 11804 articles
Browse latest View live


<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>