Category Archives: 计算机科学

Pinpoint 插件开发

一、运行 Pinpoint

运行 Pinpoint 系统最简单的方法是使用 Docker。

二、编译环境搭建

编译 Pinpoint 1.5.2 的源代码需要 JDK 6、JDK 7+ 以及 Maven 3.2.x+ 的支持,符合以上要求的最新版本的编译工具列表如下:

要求 最新版本 备注
JDK 6 JDK 6u45 已经停止更新
JDK 7+ JDK 8u112
Maven 3.2.x+ Maven 3.2.9 之所以使用 Maven 3.2.x,猜想是因为只有 Maven 3.2.x 才支持 JDK 6

并且还要设置两个环境变量:JAVA_6_HOME 和 JAVA_7_HOME 分别指向对应的 JDK 安装目录。

然后运行以下命令完成编译:

使用 Docker 来编译会更加容易,免去了配置环境的必要。首先将 Pinpoint 的源代码下载到本地目录,例如 /projects/pinpoint,然后运行命令:

其中的两个 -v 参数是用来映射目录的。第一个 -v 将本地 /projects/pinpoint 目录映射到容器的 /pinpoint 目录;而第二个 -v 是将本地的 Maven Repository 映射到容器的 /root/.m2 下,否则每次编译 Maven 都会从网络上下载大量的依赖,非常缓慢,这样在本地共享一下,以后再编译就快了。

三、技术概述

3.1、架构组成

Pinpoint 架构

Pinpoint 主要由 3 个组件外加 Hbase 数据库组成,三个组件分别为:Agent、Collector 和 Web UI。

3.2、系统特色

  1. 分布式交易追踪,追踪分布式系统中穿梭的消息
  2. 自动侦测应用程序拓扑,以帮助指明应用程序的配置
  3. 横向扩展以支持大规模的服务器组
  4. 提供代码级别的可见性,以方便识别故障点和瓶颈
  5. 使用字节码注入技术,无需修改代码就可以添加新功能

3.3、分布式追踪系统如何工作

无论 Pinpoint 还是 Zipkin,其实现都是基于 Google Dapper 的论文。其核心思想就是在服务各节点彼此调用的时候,记录并传递一个应用级别的标记,这个标记可以用来关联各个服务节点之间的关系。比如两个节点之间使用 HTTP 作为请求协议的话,那么这些标记就会被加入到 HTTP 头中。因此如何传递这些标记是与节点之间使用的通讯协议有关的,有些协议就很容易加入这样的内容,但有些协议就相对困难甚至不可能,因此这一点就直接决定了实现分布式追踪系统的难度。

3.4、Pinpoint 的数据结构

Pinpoint 消息的数据结构主要包含三种类型 Span,Trace 和 TraceId。

Span 是最基本的调用追踪单元,当远程调用到达的时候,Span 指代处理该调用的作业,并且携带追踪数据。为了实现代码级别的可见性,Span 下面还包含一层 SpanEvent 的数据结构。每个 Span 都包含一个 SpanId。

Trace 是一组相互关联的 Span 集合,同一个 Trace 下的 Span 共享一个 TransactionId,而且会按照 SpanId 和 ParentSpanId 排列成一棵有层级关系的树形结构。

TraceId 是 TransactionId、SpanId 和 ParentSpanId 的组合。TransactionId(TxId)是一个交易下的横跨整个分布式系统收发消息的 ID,其必须在整个服务器组中是全局唯一的。也就是说 TransactionId 识别了整个调用链;SpanId(SpanId)是处理远程调用作业的 ID,当一个调用到达一个节点的时候随即产生;ParentSpanId(pSpanId)顾名思义,就是产生当前 Span 的调用方 Span 的 ID。如果一个节点是交易的最初发起方,其 ParentSpanId 是 -1,以标志其是整个交易的根 Span。下图能够比较直观的说明这些 ID 结构之间的关系。

Pinpoint ID 结构之间的关系

3.5、如何使用 Java Agent

Pinpoint 的优势就在于其使用 Java Agent 的方式向节点应用注入字节码,而不是直接修改源代码。因此部署一个节点就变得非常容易,只需要在程序启动的时候加入如下一些启动参数:

3.6、代码注入是如何工作的

Pinpoint 字节码注入

Pinpoint 对代码注入的封装非常类似 AOP,当一个类被加载的时候会通过 Interceptor 向指定的方法前后注入 before 和 after 逻辑,在这些逻辑中可以获取系统运行的状态,并通过 TraceContext 创建 Trace 消息,并发送给 Pinpoint 服务器。但与 AOP 不同的是,Pinpoint 在封装的时候考虑到了更多与目标代码的交互能力,因此用 Pinpoint 提供的 API 来编写代码会比 AOP 更加容易和专业。(这些内容后面会有更详细说明)

3.7、Pinpoint 的应用实例

下图展现了两个 Tomcat 服务器应用了 Pinpoint 之后,被收集到的追踪数据。

Pinpoint 数据收集

四、Agent 插件开发

开发 Pinpoint Agent 只需要关注两个接口:TraceMetadataProvider 和 ProfilerPlugin,实现类通过 Java 的服务发现机制进行加载。

4.1、ServiceLoader 配置

Pinpoint 的插件是以 jar 包的形式部署的,为了使得 Pinpoint Agent 能够定位到 TraceMetadataProvider 和 ProfilerPlugin 两个接口的实现,需要在 META-INF/services 目录下创建两个文件:

META-INF/services/com.navercorp.pinpoint.common.trace.TraceMetadataProvider
META-INF/services/com.navercorp.pinpoint.bootstrap.plugin.ProfilerPlugin

这两个文件中的每一行都写明对应实现类的全名称即可。

4.2、TraceMetadataProvider

TraceMetadataProvider 提供了对 ServiceType 和 AnnotationKey 的管理。

4.2.1、ServiceType

每个 Span 和 SpanEvent 都包含一个 ServiceType,用来标明他们属于哪一个库(Jetty、MySQL JDBC Client 或者 Apache HTTP Client 等),以及追踪此类型服务的 Span 和 SpanEvent 该如何被处理。ServiceType 的数据结构如下:

属性 描述
name ServiceType 的名称,必须唯一
code ServiceType 的编码,短整形,必须唯一
desc 描述
properties 附加属性

Pinpoint 为了尽量压缩 Agent 到 Collector 的数据包的大小,ServiceType 被设计成不是以序列化字符串的形式发送的,而是以整形数字发送的(code 字段),这就需要建立一个映射关系,将 code 转换成对应的 ServiceType 实例,这些映射机制就是由 TraceMetadataProvider 实现的。

ServiceType 的 code 必须全局唯一,为了避免冲突,Pinpoint 官方对这个映射表进行了严格的管理,如果所开发的插件想要声明新的映射关系,需要通知 Pinpoint 团队,以便对此映射表进行更新和发布。与私有 IP 地址段一样,Pinpoint 团队也保留了一段私有区域可供开发内部服务的时候使用。具体的 ID 范围参照表如下:

ServiceType Code 全部范围

类型 范围
Internal Use 0 ~ 999
Server 1000 ~ 1999
DB Client 2000 ~ 2999
Cache Client 8000 ~ 8999
RPC Client 9000 ~ 9999
Others 5000 ~ 7999

ServiceType Code 私有区域范围

类型 范围
Server 1900 ~ 1999
DB Client 2900 ~ 2999
Cache Client 8900 ~ 8999
RPC Client 9900 ~ 9999
Others 7500 ~ 7999

4.2.2、AnnotationKey

Annotation 是包含在 Span 和 SpanEvent 中的更详尽的数据,以键值对的形式存在,键就是 AnnotationKey,值可以是字符串或字节数组。Pinpoint 内置了很多的 AnnotationKey,如果不够用的话也可以通过 TraceMetadataProvider 来自定义。AnnotationKey 的数据结构如下:

属性 描述
name AnnotationKey 的名称
code AnnotationKey 的编码,整形,必须唯一
properties 附加属性

同 ServiceType 的 code 字段一样,AnnotationKey 的 code 也是全局唯一 的,Pinpoint 团队给出的私有区域范围是 900 到 999。

4.2.3、TraceMetadataProvider 接口

TraceMetadataProvider 接口只有一个 setup 方法,此方法接收一个 TraceMetadataSetupContext 类型的参数,该类型有三个方法:

方法 描述
addServiceType(ServiceType) 注册 ServiceType
addServiceType(ServiceType, AnnotationKeyMatcher) 注册 ServiceType,并将匹配 AnnotationKeyMatcher 的 AnnotationKey 作为此 ServiceType 的典型注解,这些典型注解会显示在瀑布视图的 Argument 列中
addAnnotationKey(AnnotationKey) 注册 AnnotationKey,这里注册的 AnnotationKey 会被标记为 VIEW_IN_RECORD_SET,显示在瀑布视图中是以单独一行显示的,且前面有一个蓝色的 i 图标

详细使用方法可以参考官方提供的样例文件 SampleTraceMetadataProvider

4.3、ProfilerPlugin

ProfilerPlugin 通过字节码注入的方式拦截目标代码以实现跟踪数据的收集。

4.3.1、插件的工作原理

  1. Pinpoint Agent 随 JVM 一起启动
  2. Agent 加载所有 plugin 目录下的插件
  3. Agent 调用每个已经加载的插件的 ProfilerPlugin.setup(ProfilerPluginSetupContext) 方法
  4. 在 setup 方法中,插件定义那些需要被转换的类,并注册 TransformerCallback
  5. 目标应用启动
  6. 当类被加载的时候,Pinpoint Agent 会寻找注册到该类的 TransformerCallback
  7. 如果 TransformerCallback 被注册,Agent 就调用它的 doInTransform 方法
  8. TransformerCallback 修改目标类的字节码(例如添加拦截器、添加字段等)
  9. 修改后的代码返回到 JVM,类型加载的时候就使用修改后的字节码
  10. 应用程序继续
  11. 当调用到被修改的方法的时候,注入的拦截器的 before 和 after 方法被调用
  12. 拦截器记录追踪数据

Pinpoint 插件的工作原理看似跟 AOP 非常相似,但还是有一些区别和自身的特色:

  1. 因为 Pinpoint 需要处理的注入场景比较单一,因此他提供的注入 API 相对简单;而 AOP 为了要处理各种可能的切面情况,Pointcut 被设计得非常复杂。
  2. Pinpoint 插件拦截是通过拦截器的 before 和 after 方法实现的,很像 around 切面,如果不想执行其中一个方法,可以通过 @IgnoreMethod 注解来忽略。
  3. Pinpoint 的拦截器可以任意拦截方法,因此被拦截的方法之间可能会有调用关系,这会导致追踪数据被重复收集,因此 Pinpoint 提供了 Scope 和 ExecutionPolicy 功能。在一个 Scope 内,可以定义拦截器的执行策略:是每次都执行(ExecutionPolicy.ALWAYS),还是在没有更外层的拦截器存在的时候执行(ExecutionPolicy.BOUNDARY),或者必须在有外层拦截器存在的时候执行(ExecutionPolicy.INTERNAL)。具体请参考这个样例
  4. 在一个 Scope 内的拦截器彼此还可以传递数据。同一个 Scope 内的拦截器共享一个 InterceptorScopeInvocation 对象,可以使用他来交换数据。参考样例
  5. 除了拦截方法以外,Pinpoint 还可以向目标类中注入字段以及 getter 和 setter 方法,可以使用它们来保存一些上下文的数据。

通过上述内容可以了解,如果要编写一个 Pinpoint 的插件,除了要对目标代码的调用逻辑有较深入的理解,还必须得设计好上下文数据如何存储、如何传递,以及如何通过 Scope 避免信息被重复收集等问题。这些问题在 AOP 的场景下也会存在,只是 Pinpoint 提供了更加一致和便捷的解决方案,而 AOP 的就要自己去考虑这些问题了。

4.3.2、方法拦截

如前文所述,Pinpoint 插件需要实现 ProfilerPlugin 接口,该接口只有一个 setup(ProfilerPluginSetupContext) 方法。为了更容易的操作 Pinpoint 的代码注入 API,还需要实现一个 TransformTemplateAware 的接口,该接口会注入 TransformTemplate 类。

ProfilerPluginSetupContext 有两个方法:getConfig() 和 addApplicationTypeDetector(ApplicationTypeDetector…)。第一个方法用来获取 ProfilerConfig 对象,该对象保存了所有插件的配置信息,而第二个方法用来添加 ApplicationTypeDetector。ApplicationTypeDetector 是用来自动检测节点所运行服务的类型的。例如在 pinpoint-tomcat-plugin 项目中,有 TomcatDetector 类,这个类的作用是通过如下检测来确定当前服务为 Tomcat 的:

  1. 检查 main class 是不是 org.apache.catalina.startup.Bootstrap
  2. 检查是否有系统变量 catalina.home
  3. 检查是否存在某个指定的类,这里也是 org.apache.catalina.startup.Bootstrap

如果这三个条件都满足,就把当前节点的 ServiceType 设置为 Tomcat。

TransformTemplate 只有一个方法 transform(String, TransformCallback),第一个参数是需要被转换的类的全名称,而第二个参数就是 4.3.1 章节中提到的 TransformCallback 接口,这个接口也只有一个方法叫 doInTransform,所有的注入逻辑都在这里完成。

  1. 注入过程是从获取 InstrumentClass 类开始的。
  2. 如果想要拦截一个方法,或者是添加字段以及 getter、setter 方法,就可以调用 InstrumentClass 对应的 API 来实现,这里是获取了一个签名为 targetMethod(String) 的方法,返回的对象是 InstrumentMethod 类型。
  3. 调用 InstrumentMethod 的 addInterceptor 方法注入拦截器,所有跟踪信息的收集行为都是在拦截器中实现的,这里添加的拦截器是 com.navercorp.pinpoint.bootstrap.interceptor.BasicMethodInterceptor,这是 Pinpoint 框架默认提供的一个现成的拦截器,里面收集了一些 targetMethod 的调用信息。后面的 va 是一个静态方法,即可变参数列表,va 中给出的参数会传递到 BasicMethodInterceptor 的构造方法中。
  4. 调用 InstrumentClass.toBytecode() 方法即可返回注入后的字节码,剩下的事情就是 Pinpoint Agent 自己来完成的了。

BasicMethodInterceptor 类仅提供了对方法调用信息的简单收集,只收集方法的名称、参数、返回值以及是否产生异常等等。在某些复杂的场景下,我们会需要收集更多的信息,如当前登录用户、线程池、数据库查询语句以及任何跟中间件功能有关的信息,这就需要我们定义自己的 Interceptor 类。

以上内容请参考该样例

Interceptor 是一个标记接口,真正有意义的是 AroundInterceptor 接口,该接口定义了如下两个方法:

为了应对被拦截方法的不同个数的参数列表,AroundInterceptor 还有若干子接口:AroundInterceptor0, AroundInterceptor1,…,AroundInterceptor5,分别对应没有参数,一个参数,到 5 个参数的方法。实现 Interceptor 接口的时候要提供一个如下的构造方法:

TraceContext 和 MethodDescriptor 会被 Pinpoint Agent 运行时注入进来,当然也可以添加额外的参数,这些额外的参数,需要在 addInterceptor 的时候指定,就像上文中关于 va 的描述那样。

有了 TraceContext 对象,就可以开始收集信息了。调用 traceContext.getCurrentTraceObject() 方法可以获取当前的 Trace,再调用 trace.traceBlockBegin() 就开始记录一个新的 Trace 块了(这里我理解应该就是 Span 了)。在 traceBlockBegin 以后,可以 调用 currentSpanEventRecorder 获取 SpanEventRecorder 对象,这个对象提供了诸如 recordServiceType、recordApi、recordException 和 recordAttribute 等方法,可以记录方法的有关信息。但是 SpanEventRecorder 并没有提供 recordReturnValue 这样的方法,只能通过 recordAttribute 来记录。所有自己扩展的信息也是通过 recordAttribute 来记录的。最后所有信息记录完成就调用 traceBlockEnd() 方法关闭区块。

以上内容请参考该样例

五、总结

其实 Pinpint 的插件开发 API 还提供了非常丰富的能力,如拦截异步方法、调用链跟踪、拦截器之间共享数据等等,但原理都是基于上述这些内容,只是调用了更复杂的 API 而已。具体代码可以参考官方提供的样例项目,里面有非常详尽的代码及注释,相信理解了上面的内容,再看这个代码就不会有任何困难了。

使用 Zipkin 和 Brave 实现分布式系统追踪(基础篇)

Zipkin

一、Zipkin

1.1、简介

Zipkin 是一款开源的分布式实时数据追踪系统(Distributed Tracking System),基于 Google Dapper 的论文设计而来,由 Twitter 公司开发贡献。其主要功能是聚集来自各个异构系统的实时监控数据,用来追踪微服务架构下的系统延时问题。

应用系统需要进行装备(instrument)以向 Zipkin 报告数据。Zipkin 的用户界面可以呈现一幅关联图表,以显示有多少被追踪的请求通过了每一层应用。

瀑布图

Zipkin 以 Trace 结构表示对一次请求的追踪,又把每个 Trace 拆分为若干个有依赖关系的 Span。在微服务架构中,一次用户请求可能会由后台若干个服务负责处理,那么每个处理请求的服务就可以理解为一个 Span(可以包括 API 服务,缓存服务,数据库服务以及报表服务等)。当然这个服务也可能继续请求其他的服务,因此 Span 是一个树形结构,以体现服务之间的调用关系。

Zipkin 的用户界面除了可以查看 Span 的依赖关系之外,还以瀑布图的形式显示了每个 Span 的耗时情况,可以一目了然的看到各个服务的性能状况。打开每个 Span,还有更详细的数据以键值对的形式呈现,而且这些数据可以在装备应用的时候自行添加。

Span 详细信息

从图中可以看出如下的调用关系:整个调用链中有两个微服务 service1 和 service2,在 10ms(相对时间点)的时候,service1 作为客户端向 service2 发送了一个请求(Client Send),之后 service2 服务于 19ms 的时候收到请求(Server Receive),并用了 12ms 的时间来处理,并于 31ms 时刻将数据返回(Server Send),最后 service1 服务于 1ms 以后接收到此数据(Client Receive),因此整个过程共耗时 22ms。图中还给出了 service1 访问 service2 服务前后 Http Client 连接池的状态信息。

1.2、架构

Zipkin 架构

如图所示,Zipkin 主要由四部分构成:收集器、数据存储、查询以及 Web 界面。Zipkin 的收集器负责将各系统报告过来的追踪数据进行接收;而数据存储默认使用 Cassandra,也可以替换为 MySQL;查询服务用来向其他服务提供数据查询的能力,而 Web 服务是官方默认提供的一个图形用户界面。

而各个异构的系统服务向 Zipkin 报告数据的架构如下图。

服务向 Zipkin 报告数据

1.3、运行

使用 Docker 运行 Zipkin 最为简单,其过程如下:

这样启动,默认会使用 Cassandra 数据库,如果想改用 MySQL,可以换做以下命令启动:

启动成功以后,可以通过 http://:8080 来访问。具体获取 IP 地址的方法请参阅 Docker 的相关文档。

二、Brave

2.1、简介

Brave 是用来装备 Java 程序的类库,提供了面向 Standard Servlet、Spring MVC、Http Client、JAX RS、Jersey、Resteasy 和 MySQL 等接口的装备能力,可以通过编写简单的配置和代码,让基于这些框架构建的应用可以向 Zipkin 报告数据。同时 Brave 也提供了非常简单且标准化的接口,在以上封装无法满足要求的时候可以方便扩展与定制。

2.2、初始化

Brave 的初始化就是要构建 Brave 类的实例,该库提供了 Builder 类用来完成这件事情。

注:下文中约定,大写的 Brave 指该 Java 类库,而 Brave 类指 com.github.kristofa.brave.Brave 类型,而小写的 brave 指该类型的实例。

其中的 serviceName 是当前服务的名称,这个名称会出现在所有跟该服务有关的 Span 中。默认情况下,Brave 不会将收集到的监控数据发送给 Zipkin 服务器,而是会以日志的形式打印到控制台。如果需要将数据发送给服务器,就需要引入 HttpSpanCollector 类。当前版本(3.8.0)将这个类命名为 Collector,这个概念容易跟 Zipkin 自身的 Collector 相混淆,因此在 Issue #173 中官方建议将其更名为 Reporter,也就是说这个类是用来向 Zipkin 的 Collector 报告数据的。

Zipkin Reporter

使用 HttpSpanCollector 的方法如下:

使用 HttpSpanCollector.create 方法可以创建该类的一个对象,第一个参数就是 Zipkin 服务的地址(默认部署时的端口为 9411)。

如果使用 Spring 的话,为了方便扩展,建议添加一个名为 ZipkinBraveFactoryBean 的类,其内容大致如下:

然后只需要在 application-context.xml 配置文件中使用该 FactoryBean 就可以了:

2.3、装备标准的 Servlet 应用

Brave 提供了 brave-web-servlet-filter 模块,可以为标准的 Servlet 应用添加向 Zipkin 服务器报告数据的能力,需要做的就是在 web.xml 文件增加一个 BraveServletFilter。

不过这个 Filter 在初始化的时候需要传入几个参数,这些参数可以通过 brave 对象的对应方法获得,但是注入这些构造参数,最简单的办法还是使用 Spring 提供的 DelegatingFilterProxy。

在 web.xml 中添加如下内容(最好配置为第一个 Filter,以便从请求最开始就记录数据):

然后在配置文件中添加以下内容(创建 brave Bean 的有关代码请参考上文):

最后一个类 com.github.kristofa.brave.http.DefaultSpanNameProvider 存在于 brave-http 模块中。当使用 Maven 或 Gradle 来管理项目的话,brave-http 会随着 brave-web-servlet-filter 的引入被自动关联进来。

一切无误的话就可以启动服务。如果给定了 zipkinHost 参数,数据就会被发送到指定的 Zipkin 服务器上,然后可以在其 Web 界面上看到相关内容;否则会有类似如下的信息打印到系统控制台(做了格式美化):

2.3、装备 Spring MVC 应用

Brave 自带了 brave-spring-web-servlet-interceptor 模块,因此装备 Spring MVC 项目变得非常容易,只需要在配置文件中添加一些 interceptor 就好了:

2.4、装备 MySQL 服务

brave-mysql 模块在 JDBC 驱动层面添加了一些拦截器,可以对 MySQL 的查询进行监控。在使用之前也需要通过 Spring 进行一下配置。

该配置的目的是要给 MySQLStatementInterceptorManagementBean 类注入一个 ClientTracer 实例,这个实例会在后来的 MySQL JDBC 驱动的拦截器中被使用。初始化完成以后只需要在连接字符串中添加如下参数就可以了:

其中的 zipkinServiceName 用来指定该 MySQL 服务的名称,如果省略的话,会默认以 mysql-${databaseName} 的形式来呈现。

这里需要特别说明一点,因为 MySQL 服务是跟 Java 服务分离的,因此上文初始化 brave 对象时提供的服务名称,并不适用于 MySQL 服务,所以才需要在这里另外指定。

MySQL 服务的监控数据

可以看出,添加了 statement interceptor 之后,可以看到 service2 请求 MySQL 查询的起止时间,以及执行的 SQL 语句等信息。

三、总结

本文主要介绍了 Zipkin 服务和其 Java 库 Brave 的一些基本概念及原理,并且针对 Brave 开箱提供的一些装备组件进行了详细的使用说明。在后面进阶篇的文章中,会对如何扩展 Brave 以实现自定义监控信息的内容进行介绍,敬请期待!

企业级 IM 的差异化发展之路

钉钉与微信

钉钉 X 微信

2016 年 4 月 18 日,微信团队发布了继微信和微信支付后的第三个重量级产品企业微信,正式吹响了与阿里钉钉交锋的号角,并且还扬言:要像干掉来往一样的干掉钉钉。一时间,企业级 IM 这个既陌生又熟悉的产品,成为了互联网界热议的话题。

那么企业级 IM 到底是什么?它又跟移动办公产品有什么区别?当钉钉和企业微信展开厮杀的时候,企业级 IM 的差异化发展之路到底在何方?

既然是叫“企业级 IM”,那么不难看出其产品特性的两个着眼点:企业应用和即时通讯。企业应用一方面是应用于企业内部,另一方面也在于能够整合企业内部的各种管理流程与制度。小了来说是各种轻量级应用,大了来说可以汇聚成管理信息系统。而即时通讯就更不言而喻了,曾经当 QQ 几乎成为国内即时通讯代名词的时候,有多少企业早已不由自主的经由 QQ 群完成了许多内部的沟通事宜,可见企业还是非常需要这样一类产品的。无奈的是,无论 QQ 还是微信,其社交成分要远远大于协同办公的成分,无法成为组织内部沟通协作的首选。

当来往完败给微信的时候,阿里仍不死心的想要切入即时通讯这个腾讯最核心的业务领域,于是就此诞生了钉钉。马云更是豪赌 10 亿,另辟蹊径的从企业内用户寻找突破口,让钉钉迅速站稳了脚跟。腹地被偷袭的腾讯自然不会善罢甘休,企业级 IM 市场顿时风生水起、剑拔弩张。企业级 IM 毕竟不是什么新鲜玩意,国外的 Slack 和 HipChat 早已是行业中的佼佼者,但显然钉钉与企业微信跟 Slack 和 HipChat 看上去并不怎么相似,不只是 Copy to China 这么简单。其两者都在通讯功能的基础上增加了类似签到、审批、任务管理和文件共享等企业内常用的功能,而且还能够开放给第三方合作伙伴进行二次开发,俨然一个企业管理流程互联网化的开放平台,让很多中小企业似乎找到了协同办公的感觉,从而忘记了这些东西原本在移动办公领域已经是必备的功能了。

然而企业内部的系统,必然不同于互联网化的系统。一个组织并不会因为“系出名门”、“免费”或“更加易用”等特性就放弃正在使用的古老系统;而且企业内部的管理流程也远非轻量级应用所能覆盖,尤其是在以流程驱动管理的企业内部,动辄十几步甚至几十步的流程都大有所在,而且流程信息量也绝非任务管理这等规模的应用所能比拟的;最后无论钉钉抑或企业微信,都还仅仅是将即时通讯和轻量级应用做了一个简单的叠加,流程数据还存在流程中,而碎片化的聊天信息依旧隐含着大量的珍贵知识,企业级 IM 是否应该走得更深入一些。

文章 An Inside Look at a Flat Organization That Serves Millions 的作者 Nate Lee 在介绍如何管理一个扁平化组织的时候,提出了一个命题: Assume that new hires won’t be able to talk to any of their co-workers for 48 hours. This is of course never the case, but if it was, they should have enough information at their fingertips to do their job. They should be able to read up on any problem or feature or question they have.(假设新来的员工在 48 小时内不能和其他同事讲话。在这种假定下,他们应该动动手指就能得到足够的信息去开展工作。如果在工作中遇到了问题,他们也应该能够通过阅读自己弄明白。)结合 Nate Lee 在文章中给出的解决方案,外加我个人的一些理解,我觉得企业级 IM 应该可以往如下一些方向发展。

一、流程与消息的整合

与其在企业级 IM 内部实现轻量级应用,不如让其与现有系统做更好的集成。在这一点上不能不提 HipChat,她与 Bitbucket、Jira 以及 Github 都有非常完美的集成,能够在上述系统发生变化的时候以即时消息的形式推送给用户,而且开放的 API 也使得越来越多的第三方系统能够与其整合,使其功能日益强大。如果真的要在企业级 IM 内部嵌入流程应用,那么就该想想如何实现融合。

想象一个场景,开发人员在写程序的时候通常都要关注变更管理系统,而当一个问题并没有被很好描述的时候,开发人员要么像回复论坛中的帖子一样提出问题,并等待答复(就像 Github 那样),要么就更直接拉几个相关的人进入一个讨论组开始聊天,直到把问题讨论清楚。这两种方式显然都不是最优的。第一种方式就好像在使用邮件,邮件虽然更正式、更严谨,但是受到排斥的根本原因就是时效性,不能够“即时通讯”,只能书写然后等待。但是无疑所有的交流痕迹都会被保存下来,当日后需要回顾的时候,查阅就变得异常方便了;第二种方式解决了时效性的问题,但最严重的缺陷就是有可能丢失所有针对该问题的讨论细节,仅仅会把一些结论性质的东西更新到问题的描述中,更有可能连这些结论性质的东西都会随着懒惰与监管的不到位逐渐流失。

或许想象中的系统应该是这样工作的。当开发人员需要针对某个问题展开讨论的时候,可以直接针对这个问题发起一个话题,工具应该有能力自动将相关人员拉入这个话题讨论组,并定位到当前的流程状态。所有接下来的讨论均会被这个话题记录下来,并关联到当前状态上。某些核心的讨论结果还可以直接更新回或添加到问题的描述信息,一次交流就解决了历史留存和沟通时效的问题,将来再回过头来查看的时候,每个状态的每个时间点上都有若干聊天记录呈现在旁边,辅佐查看问题的人了解达成最终结论的演变过程。

不仅如此,即时通讯除了可以解决沟通上的时效性以外,结合流程的时候还能够提高流程的工作效率。就拿最简单的报销流程来讲,倘若一个报销事宜经由申请人填报、主管经理审批、部门总监审批、财务人员审批最后到公司负责人审批这样的流程。若其中任何一个审批环节出现问题,都会导致流程被退回。虽然这本身正是流程存在的意义,但是显然被退回的流程一定程度上影响了流程的运作效率。如果填报人员或某一级审批人员在处理信息的过程中发现有疑问,那么针对当前环节上的流程进行一番讨论,消除分歧,可以促使流程走到终点的可能性大大提高。

二、话题

其实上面的能力都离不开一个功能就是话题,可以有针对流程的话题,也可以有自由创建的话题。话题无非限定了群组里面讨论的内容,当一个话题讨论完成以后,就应该将其关闭,避免发生偏离。在需要的时候还可以将话题之间进行关联或者是让话题与流程、文档抑或某个用户进行关联。话题将碎片化的消息进行了归类与划分,多维度的话题关联让话题有了更多的延展,同时还能加快话题的定位速度,久而久之一个由话题构建成的知识库就慢慢诞生了,无需再特意去整理这些交流过程中显现出来的隐性知识,后来者可以非常方便的经由话题逐渐深入了解组织正在发生的事情。

三、个人助理

个人助理的想法是由微软小冰带来的灵感,她就像是一个逗逼的邻桌,只要你跟她说话,她就总能出乎意料的给你反馈。既然企业级 IM 定位在了聊天,那么为什么我还要去应用中开始某个八竿子打不着的流程能?如果每个人都有一个像微软小冰那样的助手,你可以跟她说需要走一个流程、需要记录一份文档、需要展开一个话题甚至需要进行一次电话会议,她会应该帮你把余下的事情都处理妥当,你需要的就等待最后的结果。Tony Aube 在 No UI Is The New UI 中对未来的人机交互给出了大胆的假设,想必以后需要做什么事情之前都最好找你的机器人助手帮个忙。而且时间久了她还会更了解你,你无需再跟她说复杂的指令来让他做一个事情,可能仅仅一个字,甚至一个动作她就知道了你的初衷。

还是以报销作为容易理解的场景,你直接跟你助手说你要发起一个报销,她会告诉你报销的注意事项,以及最近是否有制度上的更新。然后她会根据权限和组织关系把此次报销的相关人员都拉入一个话题,你直接说出你要报销的条目,她会帮你将其整理成一份标准格式的表单,并提交到流程系统,同时知会相关人员。如果需要讨论就地就可以进行,而且信息会随着讨论的进行逐渐更新,直到最后你确认提交此次申请,其他人可以同时就完成审批。

四、状态更新

我们平时都喜欢更新 QQ 或微信的状态以及签名档,好让朋友们知道我们现在的心情或者正在做的一些事情,这个功能拿到工作中也同样适用。哪个领导不想知道自己的下属在做什么,难道要每天去问问看吗?当你在处理某项工作、打开某个文档抑或参与某个话题的时候,所有的状态都会自动进行更新,主管领导就可以看到每个人的当前工作内容,不会有人再不停的追问你在做什么,你的工作上的一切(包括发呆)都会被清楚的记录下来(可能会有种被窥视的感觉,不过汇报工作内容本来就是需要的,把控好力度就 OK 了)。

最后,所有这些设想中的功能都需要即使通讯与企业应用做到有机的结合,这本来也该是企业级 IM 天生具有的能力。这些异想天开的功能,嘴上说说容易,但实现起来还需要一段漫长的路,甚至是颠覆用户体验的变革。企业级 IM 产品必然会迎来一次集中爆发,让我们拭目以待。

如何去掉 OSX 系统中打开方式里的重复项

在 Terminal 里面执行如下命令:

之后重启 Finder 就可以了。

提升 Jetty 服务器的启动速度

最近在使用 Jetty 调试一个系统的时候发现,控制台会长时间停在某个地方,致使系统无法正常启动。经过调整日志的输出级别后发现,Jetty 有一个 AnnotationParser 的类,会去扫描 classpath 中所有的类文件,而碰巧这个项目有一个 20M 大小的 jar 包,就导致这个扫描过程异常缓慢,使得 Jetty 无法在短时间内里面完成启动。

要说明 AnnotationParser 类的作用,需要先从 Servlet 2.5 的规范说起。2.5 版的 Servlet 规范最重要的是引入了对 Java EE Annoation 的支持,也就是可以用注解来声明 servlet、filter 等对象。这件事前以前是在 web.xml 文件中完成的,现在可以把 web.xml 中的定义全部移植到 Java 类里面,而不引入任何配置文件。这就导致一个问题:作为 Servlet 容器的 Jetty 怎么能够知道哪些类使用了这些注解?很显然,并没有非常有效的办法,因此 Jetty 才实现了一个 AnnotationParser 的类型,通过遍历的方法来寻找所有使用了注解的类型。

如果我们开发的程序没有使用注解,这个过程就是纯粹在浪费时间,Servlet 规范也提供了一种办法禁用此种类型的扫描,从而提升启动速度。方法就是在 web.xml 文件中增加 metadata-complete 配置。

需要注意的是 web.xml 中声明使用的 version 属性一定要大于 2.5 版,才有这个配置。

参考资料:

在 Sourceforge 项目空间上部署 MediaWiki 2

《在 Sourceforge 项目空间上部署 MediaWiki》的第一部分在这里。

造成 MediaWiki 不能向 config 目录写文件的原因,估计是 Sourceforge 服务器没有开放这样的权限。幸亏,MediaWiki 提供了一个变通的办法。找到下面的这些代码,把他们注释掉:

这样一来,当目录没有写权限的时候,操作不会被取消,而是生成的配置文件会以文本的形式被打印在页面上。把这些代码拷贝、粘贴到一个名为 LocalSettings.php 的文件,然后上传到 MediaWiki 的根目录,在我们这个例子中也就是 /home/groups/s/sa/sample/htdocs/wiki/ 就可以了。

最后还需要解决一下 session 的问题。由于 Sourceforge 使用了服务器集群,所以把 session 保存在内存中是有问题的,所以需要用文件来存储 session:

在 /tmp/persistent/ 下面创建一个用来保存 session 的目录,例如 /tmp/persistent/sample/sessions。这里的 sample 就是你的项目的 unix name。因为这个名称是唯一的,所以不用担心会跟别人的重复。但注意千万不要把它建在你的 project 目录下,因为他们是不能被 web 服务器写入的。

确保这个目录是可写的,也就是执行一下 chmod a+w /tmp/persistent/sample/sessions。sample 仍然是项目的 unix name。
告诉 MediaWiki 使用这个目录来存放 session,方法是在 LocalSettings.php 文件的开始处添加 session_save_path(“/tmp/persistent/sample/sessions/”); 注意不要丢掉 sessions 后面的那个 “/”。如果你这样做之后,在编辑或查看页面的时候退出,碰到空白页面,请把 ini_set( “include_path”, “.:$IP:$IP/includes:$IP/languages” ); 添加在上一步的 session_save_path 之前。

这样,你的 Wiki 就基本上可以在 Sourceforge 的项目空间上运行了,不过还有很多问题这里没有涉及到,比如:如何配置邮件系统、如何修改网站的 logo、或者是一些碰到的其他的问题。更多内容请详见 Running MediaWiki on Sourceforge.net。(中国大陆地区访问可能会有障碍。)

在 Sourceforge 项目空间上部署 MediaWiki

在开始部署之前,有一些准备工作要做:

你需要在 Sourceforge 上面拥有自己的帐号,并加入到一个项目,而且你的帐号在该项目中需要有访问 shell 的权限。同时确保该项目已经开通 MySql 数据库。

到 MediaWiki 的官方网站去下载 MediaWiki 1.6.8 版本。注意千万不要下载最新版本,因为 MediaWiki 从 1.7 版以后需要 php 5 的支持,而 sourceforge 服务器上部署的仍然是 php 4,因此无法运行最新版本。

从 PuTTY 网站上下载 PuTTY.exe 和 PSFTP.exe 。

为了方便,假设我在 Sourceforge 上拥有用户 usr,密码为 pwd,而且参与的项目为 sample。该项目的 MySql 数据库具有读写权限,可以添加、删除表和索引,并可以锁定表的用户为 mysqlusr,密码 mysqlpwd。

把下载来的 mediawiki-1.6.8.tar.gz 文件上传到项目空间,具体做法如下:

  1. 运行 psftp.exe
  2. 执行 open shell.sourceforge.net
  3. 按照提示输入你在 sourceforge 上注册的用户名,例如 usr
  4. 输入用户密码,例如 pwd
  5. 进入项目的 htdocs 目录,执行(以 sample 项目为例) cd /home/groups/s/sa/sample/htdocs/
  6. 把 mediawiki-1.6.8.tar.gz 复制到 psftp.exe 所在目录
  7. 运行 put mediawiki-1.6.8.tar.gz
  8. 待上传完成,关闭 psftp

解压已经上传的 mediawiki-1.6.8.tar.gz 文件:

  1. 运行 putty.exe
  2. 在 PuTTY Configuration 界面的 Host Name 中输入 shell.sourceforge.net,确认 Protocal 选中 SSH,然后点 Open
  3. 按照提示输入用户名,例如 usr
  4. 然后输入用户密码,例如 pwd
  5. 进入项目的 htdocs 目录,执行(以 sample 项目为例) cd /home/groups/s/sa/sample/htdocs/
  6. 执行 tar -xvzf mediawiki-1.6.8.tar.gz
  7. 将解压后的目录重命名为 wiki,执行 mv mediawiki-1.6.8 wiki

登录 phpMyAdmin,创建一个 Database,例如 wikidb。当然用 sourceforge 提供的 MySql 服务创建 Database 时,命名都需要有一个前缀,在此忽略。

打开浏览器,登录项目 Wiki,例如 http://sample.sourceforge.net/wiki。此时由于 MediaWiki 尚未配置,信息提示让你进入配置页面,点击进入配置页面,发现出错了。大意是说你的 config 目录没有写权限,让你在 MediaWiki 的安装目录执行 chmod a+w config 命令更改权限。但是,尽管你按照提示完成了设置,错误依旧。

本文的第二部分在这里。

互联网 = 搜索 + 社区

为什么这么多互联网公司都在不遗余力地做着重复性劳动,看看这个公式也许就有点理解了。

什么是搜索?一个汇集互联网内容的大型工具。网络是自由且开放的,但是你不去找他,他不会来找你。拥有了搜索,就是拥有了互联网中所有的资源,就相当于拥有了一个无比庞大的集贸市场。

但一个再大的集市,如果没有人也是不顶用的。这就是社区。也许说“社区”更容易让人理解“人”在其中所处的地位。当然我更倾向于使用一个目前互联网上发了烧的词语,那就是 Web 2.0。当然 Web 2.0 不同于社区,但我理解中的 Web 2.0 同样是以人为本的。他更强调信息的收集、共享和流通,这些动作都是以人为主体的。所以 Web 2.0 是一个人问概念,而绝非异步 Javascrip 和 XML 这么简单。

举个小例子,就拿刚刚结束的世界杯来说,订阅比赛结果应该是大家都享受过的服务。那么,你想没想过:当一场比赛结束之后,你订阅的信息怎么就发生变化了呢?也许这个问题太弱智了,你都不屑回答:当然是有人改过的。那么好我不否认你的答案,世界杯人人关注,而且信息量小,会有一些人花上一两秒的时间中去更新一个比分,你在服务的下游自然就收到了结果。但是倘若一个信息量庞大的服务,比如网络音乐,无数的歌手,无数的专辑,我想不会有人愿意专门去做更新这样的事情。但是你可能很愿意为你喜欢的歌手或专辑花一些时间去整理。想要把这些零星的内容组织起来,形成一个有规模的服务,就只能靠搜索了。

拥有了搜索是拥有了内容,拥有了社区就是拥有了人。人和内容都有了,互联网也就在如来佛的手掌中了。

【转载】你所使用的浏览器反映出了你的个性

firefox_bites_ie_awesome.jpg

我经常根据一个人所使用的浏览器来评价某个人。不管你信不信,你对浏览器的选择往往反映出了你的个性。

IE 5.0:

你使用电脑仅仅是为了即时聊天,写写电子邮件和博客。你顽固地拒绝升级你那老旧的 WIN98,因为你并不需要太多的功能而且认为 WIN98 已经工作地很好了。你同时可能不使用任何杀毒软件,你只是每个月让你的儿子,侄子或朋友把把病毒清理干净而已。

IE 6.0:

你很可能并不知道什么叫做“浏览器”并且认为 IE 就是因特网。你对技术没有清晰的概念,而且你通常对电脑感到畏惧。同样的,你使用电脑也仅仅是为了即时聊天,写写电子邮件和博客。也许你的朋友曾不断地向你提及“被炒鱿鱼的狐狸”(Fired Fox),但你一直不明白那到底是什么,也不准备在它上面花时间。

IE 7.0:

你认为你站在了技术的最前沿,同时认为微软是地球上最伟大的公司。至于那个邪恶的 “Lenoux” 操作系统(音同 Linux)则是由恐怖分子编写出来的。你在卧室的墙上张贴了斯蒂夫·鲍尔默(微软首席执行官)的海报,并希望自己在未来能成为第二个比尔·盖茨。你一想到 “Vista” 便会激动地浑身颤抖、坐立不安。

Firefox 1.x:

你很可爱而且有点傻里傻气的,并为 FireFox 感到骄傲。你是开源运动的强烈支持者,你认为理乍得·马修·斯托曼才是“真正的男人”。你其实并不关心 FireFox 是不是比 IE 更安全,更快速——你会一直使用 FireFox 哪怕它的效率比 IE 低上十倍。你只是因为你得到了一个免费、开源并拥有庞大技术支持社区的浏览器而感到高兴。无论任何时候你都会安装至少 7 个必不可少的扩展。

Firefox 2.0 Beta:

在白天你是个程序员,到了晚上你就成了一个开源软件开发者。要不,你就是一个疯狂的 Firefox 粉丝。你热衷于上报你遇到的每一个 Bug,很可能你已经发布了至少一个开源项目的补丁。你喜欢对程序修修补补,而且丝毫不会在意在自己的电脑上运行 beta 版软件。毕竟,发现新的 Bug 和修改最新的软件对你来说充满了乐趣。

Mozilla:

从一开始你就在使用 Mozilla。你认为 FireFox 宣传地过了火,相对于 FireFox 你更愿意去使用旧版的 Netscape。你并不认为 Mozilla套装(Moz Suite)是个负担——事实上你更喜欢一个集成了邮件客户端、IRC 聊天客户端和网页编辑器的浏览器。你很不理解为什么有些人宁愿去挑选一个功能很少的浏览器而不是选择 Mozilla。在其他的方便你更像一个 Firefox 用户——你喜欢开源、你喜欢你的浏览器扩展、等等——或许你会说 Firefox 用户的口味和你非常相似。总之,你们在使用一个令人钦佩的、功能强大的、gecko 内核的浏览器,与此同时很多人仍然在他们的 IE 浏览器里挣扎。

Opera:

你并不关心 Firefox 之流,你所需要的只是一个世界上最好的浏览器——对你来说那就是 Opera,而你很可能早在 Opera 收费时就购买了它。如果有一个 Firefox 粉丝对你的浏览器评头论足,你就会打开一个 ACID2 测试,然后以此来驳倒他。你知道什么是你所需要的(一个快速、支持标准的浏览器),你也明白怎样得到它。你对浏览器大战丝毫不感兴趣,虽然你有一点点希望
Firefox 获胜,因为如果那样的话会有更少的网页开发者制作只兼容 IE 的页面。

Netscape 8.x:

你是一个刚刚得到一台新电脑的老资历网民,虽然你对互联网知道的并不多,但你却清楚地记得你需要 Netscape 去使用它。你并不明白人们谈到的 IE 和那个叫 Fire 什么的东西到底是什么,而且搞不清楚奥普拉·温弗瑞(Oprah,一个脱口秀主持人,音同 Opera)和因特网有什么关系,你所知道的就是点开那个大大的 “N”,然后变成”在线”。你认为史蒂文的关于网络的演讲很有道理。

Netscape 7 和更老的版本:

参见 IE 5.0。

AOL Explorer:

曾经有一天你安装了最新的 AIM 客户端,然后这个东西就成了你的默认浏览器。你非常讨厌它,但你却不知道怎样才能把它变回去。你甚至不知道你怎样才能向你的那些电脑高手朋友们描述这个问题,当你想得到帮助时你也许会像这样提问:“你能把这个新的网络,呃,变回原来的那个旧网络吗?”他们只会瞪着你,然后装作不明白你在说什么。他们或许并不像他们自己所说的那样了解电脑。

AOL Suite:

你很可能仍然在使用 AOL 的拨号网络,不然的话,你就是觉得在你使用宽带网络之后仍然需要 AOL。有人告诉过你其实你上网是不需要用 AOL 拨号的,但你无法想象这是怎么一回事。这看起来很难做到,而且似乎是非法的。

Safari:

恭喜你!你是一个 Mac 用户并享受着那个名字给你带来的好处和好心情。你喜欢 OSX,并且永远不会使用 Windows。Windows 对你来说实在是太过丑陋和低效,你更喜欢 Mac 的简洁和清晰,而 Safari 就是一个为你工作的浏览器。你从不会烦心去寻找另一个浏览器,因为你对你现在拥有的一切已经非常满意,你也不会因为世界而改变它。

Konqueror:

你是一个 linux 用户,并且打心底就是个极客(?)。你认为 KDE 是最好的桌面环境,并且因此而鄙视 Gnome。你喜欢一个同时是文件管理器、ftp/scp 客户端、smb 分享客户端、PDF 文档查看器和其它很多东西的浏览器。你喜欢向你的朋友炫耀 KDE 的网络透明度,你仅仅通过浏览器在你的网页服务器上编辑一个 HTML 文件,保存它,然后又在浏览器里重新载入修改后的文件。你日常使用的绝大多数软件都以 K 字大头(Kmail, Kontact, Kdevelop, Koffice 等等)。

Lynx:

你肯定是个骗子,你真的想让我相信你使用一个文字浏览器来浏览所有网页?尤其是一个不支持 javascript, frames, css 甚至连 tables显示都有问题的浏览器?说真的,我可以相信你一直使用 VI(一个编辑器),用 Mutt 或 Pine 做你的主要邮件客户端,但你不可能让我相信你使用 lynx 作你的主浏览器。如果你真的做到了,那么你就是我一生中见到过的最最执着的极客了。向你脱帽致敬!

如果你不同意上面的话,请留言好让我知道。如果你被我不幸言中,那么请停止使用那个该死的浏览器并换一个真正的浏览器吧。也请你自由地给我漏掉的浏览器作简短的描述。

免责声明:我不清楚是谁制作的那个 FireFox 图像(就是本文开头的那个),有个人在留言本中曾使用它作头像。我向那个作者致以崇高的敬意,如果我能找到他的话。

谢谢你们所有的评论,让我们开公布诚吧——我并没有说 Lynx 是一个差劲的浏览器,事实上我在很多不同的方面都经常使用它。我只是怀疑是否有人把它作为主浏览器。如果你是的话,向你脱帽致敬!你比我执着多了。

现在我补充一些漏掉的比较流行的浏览器:

Flock:

他们也许会称你为 Web 2.0 先生。你所使用浏览器表明一点:你的足迹遍布 flickr, del.icio.us, youtube 和其它一打的网站。你认为 Firefox 还不错,但它并不不能让你在弹指间就能完成写博客、照片共享、标签和网络书签等等功能。你希望在你的脑袋里植入一块芯片,这样你就能一直连接到网络,而且能使用 24/7 移动博客。当一些目光短浅的人告诉你 Flock 只不过是 Firefox 的修改版时,你会赶走他们并说他们不能以更宽广的视野看东西。

Epiphany:

你是一个 Gnome 用户并为之自豪。你认为 KDE 简直是地狱里出来折磨人的东西,并且热衷于向人们解释 KDE 必须经过几个小时的修改才能使用,至于那些说 KDE 马上就能用的人则是可耻的骗子。你希望所有东西能更加简单和直观——那就是你为什么选择了 Gnome,这同样也是你使用 Epiphany 的原因。你试过 Mozzila 和 Firefox,但你发现它们实在是臃肿、丑陋和麻烦。你的桌面就像你的书桌一样整齐有序。

Maxthon and Avant:

你也许有些疑惑,虽然你喜欢IE并且不会换用别的浏览器,也不会担心网站会出现渲染不正常的错误,更不会担心它像其它内核浏览器那样不支持 ActiveX 控件,但你心底还是羡慕那些使用可以做到标签页浏览和其它很酷的功能的浏览器的朋友。你承认 IE 有点落后于时代,而你想要一些更加现代的东西,同时也不想放弃正常显示一些网站。Maxhton/Avant 让你拥有了世界上最好的两项功能——舒适温暖的IE渲染引擎和其它浏览器里非常酷的功能。当 IE7 发布正式版时你就会换用 IE7。

Sea Monkey:

你很喜欢简单的软件套装,对你来说把浏览器和电子邮件客户端分开是不可理喻的。你以前习惯于使用 Mozilla,但 Sea Monkey 发布后你很快便换了口味并不再回头。你认为“Sea Monkey”是浏览器中有史以来最酷的名字。

w3m:

你一生的大部分时间都在当系统管理员。你很少看见阳光,因为你一天中大部分时间花在大型服务器的周围。如果周围没有电脑风扇的“嗡嗡”声你便无法入睡。哪怕是在夏天你每天也不得不穿一件暖和的夹克,因为服务器机房里的冷气开的是如此之高,如果你不加以注意的话便很容易感冒。年轻的极客们都向你看其,并试图模仿你——而你一直也不知道这是为什么。

K-Meleon:

你对长时间等待浏览器启动感到很不耐烦,甚至IE的启动速度对你来说也太慢了点。这也是为什么你的浏览器会预读取页面,然后仅仅花费十亿分之一秒去载入页面。你的生活节奏非常快,根本没有时间去等待浏览器慢慢启动。你可以花费几个小时去设置 Windows 注册表来提升程序的响应速度、载入时间,并减少所有程序的超时时间。

Dillo:

你从心底就是一个喜欢低资源占用的人,你喜欢让你的程序更加小巧和快速。你最喜欢运行 IceWM 或 Windowmaker,同时嘲笑那些臃肿的桌面环境像 KDE 或 Gnome。你以本地 Linux/BSD guru 著称。

中文翻译地址 一点笔记

英文原文地址 Terminally Incoherent