• Home

零侵入微服务日志追踪(五):网关

微服务横行的年代,网关已经成为微服务的标配。作为服务的入口,网关在请求进去实际业务集群前,可以做很多公共的事情,使得业务微服务更加的专注于业务,比如统一的鉴权、限流、熔断保护、降级。

但其实网关还可以做一件很重要的事:给每个响应增加TraceId。把TraceId返回给调用方,在前后端联调、开放接口出错等情况下,客户只需要把TraceId告诉接口负责人,负责人便可以利用APM系统、日志系统进行精准定位,极大的加快问题的定位和分析速度。

为例保证无侵入,网关可以把TraceId写入额外的HTTP Header,放回给调用方,这样就不需要解析业务微服务的响应体。

我们以Zuul为基础实现api网关,借助前面提到的Pinpoint无侵入的生成唯一TrxId。TrxId格式$agentId^$startTimestamp^$seq,$agentId为应用名-主机IP,$seq表示这个请求是这个实例处理的第$seq个请求,$startTimestamp是实例启动时间。TrxId包含了较多的敏感信息,不易直接暴露给客户端。因此可以做个映射,TrxId <-> TraceId,输出不含敏感信息的TraceId。

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.vanke.gateway.common.constants.Constants;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletResponse;
import java.util.UUID;

@Component
public class TraceIdPreFilter extends ZuulFilter {

    protected static final Logger LOG = LoggerFactory.getLogger(TraceIdPreFilter.class);

    @Override
    public String filterType() {
        return "pre";
    }

    @Override
    public int filterOrder() {
        return 2;
    }

    @Override
    public boolean shouldFilter() {
        return true;
    }

    @Override
    public Object run() {
        // 更新MDC内的ptxId,不能删除
        // 由于线程复用,MDC可能是上个请求的trx信息,强制调用一次log,可以更新MDC的内容为本次请求对应的trx
        LOG.info("生产内部TraceId");
        String traceId = UUID.randomUUID().toString().replace("-", "");
        RequestContext context = RequestContext.getCurrentContext();
        context.set(Constants.REQ_CTX_TRACE_ID, traceId);

        String ptxId = MDC.get("PtxId");
        if (StringUtils.isBlank(ptxId)) {
            return null;
        }
        LOG.info("设置X-Trace-Id: {}, 关联pinpoint trxId: {}", traceId, ptxId);
        RequestContext ctx = RequestContext.getCurrentContext();
        HttpServletResponse resp = ctx.getResponse();
        resp.setHeader("X-Trace-Id", traceId);


        return null;
    }
}

输出示例如下:

通过ELK可以找到对应的Pinpoint TrxId:

利用Pinpoint TrxId就可以进行更进入的追踪了。

零侵入微服务日志追踪(四):ELK做日志分析

上文介绍了利用Pinpoint把trxId以低侵入方式注入到每行日志输出中,方便在日志文件内进行单个请求的日志识别。

但是依然没有解决一个问题是:快速找出某个请求涉及的所有服务的日志内容。

可以利用ELK这类分布式日志系统,自动收集所有服务、所有实例的每个日志文件,统一存储和索引。然后利用ES的查询语句,进行全局的搜索(精确条件过滤、文本模糊搜索)。

由于我们已经把Pinpoint trxId写入日志行中,因此可以对trxId进行索引。这样我们就可以利用ptxId精确的搜索到一个请求的链路经过的所有服务实例产生的日志了,极大的提升了日志定位的速度。

上图展示了一个涉及两个服务(api-gateway、authserver)的请求。

如何获取一个接口调用的trxId将会在下一篇文章介绍。

零侵入微服务日志追踪(三):pinpoint

dapper

Dapper是google的分布式日志追踪系统。在谷歌公开的关于dapper的论文中,阐述了其分布式追踪的原理。dapper在Correlation Id(论文中称为Trace Id)的基础是引入了跟踪树。Span表示每一个调用,记录Span之间父子关系。同一个事务的所有Span都挂在一个特定的跟踪上,也共用一个Trace id。所有这些ID用全局唯一的64位整数标示。Span有关联的时间信息和业务注释信息(Annotation)。

topu

以上图调用拓扑为例,转换为调用树如下

tree

一个Span的细节如下

spandetail

上图中这种某个span表述了两个“Helper.Call”的RPC(分别为server端和client端)。span的开始时间和结束时间,以及任何RPC的时间信息都通过Dapper在RPC组件库的植入记录下来。如果应用程序开发者选择在跟踪中增加他们自己的注释(如图中“foo”的注释)(业务数据),这些信息也会和其他span信息一样记录下来。

Pinpoint

Pinpoint是Google dapper模型的一个实现,但是有所不同。

Pinpoint 实现追踪的消息的数据结构主要包含三种类型 Span,Trace 和 TraceId。
1.Span:是最基本的调用追踪单元,当远程调用到达的时候,Span 指代处理该调用的作业,并且携带追踪数据。为了实现代码级别的可见性,Span 下面还包含一层 SpanEvent 的数据结构。每个 Span 都包含一个 SpanId。
2.Trace:是一组相互关联的 Span 集合,同一个 Trace 下的 Span 共享一个 TransactionId,而且会按照 SpanId 和 ParentSpanId 排列成一棵有层级关系的树形结构。
3.TraceId:是 TransactionId、SpanId 和 ParentSpanId 的组合。
4.TransactionId: (TxId) 是一个交易下的横跨整个分布式系统收发消息的 ID,其必须在整个服务器组中是全局唯一的。也就是说 TransactionId 识别了整个调用链;TxId由AgentId、JVM启动时间戳和消息在此实例的序列号组成。
5.SpanId: (SpanId) 是处理远程调用作业的 ID,当一个调用到达一个节点的时候随即产生;
6.ParentSpanId: (pSpanId) 顾名思义,就是产生当前 Span 的调用方 Span 的 ID。如果一个节点是交易的最初发起方,其 ParentSpanId 是 -1,以标志其是整个交易的根 Span。下图能够比较直观的说明这些 ID 结构之间的关系。
7.AgentId:JVM实例的名字,由用户设定,全局唯一。实践中,我们采用应用名加主机名。


dapper

Pinpoint和google dapper的不同:
1.Pinpoint的TransactionId就是Google Dapper中的trace id
2.PinPoint中的Trace id是一值一组不同的ID,如上图所示

Pinpoint运用JavaAgent字节码增强技术,只需在JVM启动时加上一些参数即可,业务代码无需任何修改。实践中,由于我们是对遗留系统进行架构升级,因此我们首选Pinpoint这种零侵入、低开发成本的方案。

运行是额外的启动参数:
-javaagent:pinpoint提供的agent jar包的路径
-Dpinpoint.agentId:JVM唯一标识(应用实例,实践中,我们采用单机多应用部署,因此采用应用名加主机名)
-Dpinpoint.applicationName:应用名(微服务名,采用CMDB中统一服务名)

示例:

java -javaagent:/opt/pinpoint/pinpoint-bootstrap-1.6.0-RC1.jar -Dpinpoint.agentId=api-gw-10.0.0.1 -Dpinpoint.applicationName=api-gateway -jar api-gateway-1.0.0-SNAPSHOT.jar

一个服务调用及Pinpoint展示的跟踪树的例子:
td_figure5

Pinpoint与logback/log4j

Pinpoint的零侵入其实是建立在提前以插件形式对常用库的JVM增强基础上的。其中日志常用库log4j和logback都有相关plugin。以logback为例,pinpoint-logback-plugin插件会把txId和spanid存到logback的MDC中,因此只需要在logback的配置文件中设置layout为输出MDC中txId和spanId即可。


其中PtxId和PspanId即为pinpoint-logback-plugin插件存入MDC的两个属性。
默认情况下,Pinpoint并不是跟踪所有的请求,而且不把txId和spanId存入MDC,因此需求修改pinpoint的配置文件$AGENTPATH/pinpoint.config

# 修改采样率为100%(如果不设置为100%,则会有请求不会被trace)
profiler.sampling.rate=1
# 把transaction信息存入logback MDC
profiler.logback.logging.transactioninfo=true
# 把transaction信息存入log4j MDC
profiler.log4j.logging.transactioninfo=true

输出日志示例:

[2018-03-13 15:09:47.953] INFO [qtp1861416877-61] c.y.p.a.g.z.f.MethodExistedPreFilter [TxId:gateway^1520924888825^1, SpanId:-9016575308619697817] - 检查/gateway/oauth2/token/refreshToken是否存在对应接口定义
[2018-03-13 15:09:47.979] INFO [qtp1861416877-61] c.y.p.a.g.z.f.MethodExistedPreFilter [TxId:gateway^1520924888825^1, SpanId:-9016575308619697817] - /gateway/oauth2/token/refreshToken对应的接口定义: com.**.web.AuthorityService#refreshToken
[2018-03-13 15:09:47.983] INFO [qtp1861416877-61] c.y.p.a.g.z.f.RateLimitPreFilter [TxId:gateway^1520924888825^1, SpanId:-9016575308619697817] - 开始限流检查:0:0:0:0:0:0:0:1 /gateway/oauth2/token/refreshToken
[2018-03-13 15:09:47.983] INFO [qtp1861416877-61] c.y.p.a.g.z.f.RateLimitPreFilter [TxId:gateway^1520924888825^1, SpanId:-9016575308619697817] - 接口:/gateway/oauth2/token/refreshToken 不限流
[2018-03-13 15:09:47.984] INFO [qtp1861416877-61] c.y.p.a.g.z.f.AuthPreFilter [TxId:gateway^1520924888825^1, SpanId:-9016575308619697817] - /gateway/oauth2/token/refreshToken 开始认证
[2018-03-13 15:09:47.984] INFO [qtp1861416877-61] c.y.p.a.g.z.f.AuthPreFilter [TxId:gateway^1520924888825^1, SpanId:-9016575308619697817] - 不需要认证

[TxId:gateway^1520924888825^1, SpanId:-9016575308619697817]即为pinpoint生产的traceId,其中TxId即为全局唯一请求ID。

参考资料:
1.http://research.google.com/pubs/pub36356.html
2.https://bigbully.github.io/Dapper-translation/
3.http://naver.github.io/pinpoint/techdetail.html
4.http://www.tangrui.net/2016/zipkin-vs-pinpoint.html

零侵入微服务日志追踪(二):MDC

MDC

MDC(Mapped Diagnostic Context,映射调试上下文)是 log4j 和 logback 提供的一种方便在多线程条件下记录日志的功能。

某些应用程序采用多线程的方式来处理多个用户的请求。在一个用户的使用过程中,可能有多个不同的线程来进行处理。典型的例子是 Web 应用服务器。当用户访问某个页面时,应用服务器可能会创建一个新的线程来处理该请求,也可能从线程池中复用已有的线程。在一个用户的会话存续期间,可能有多个线程处理过该用户的请求。这使得比较难以区分不同用户所对应的日志。当需要追踪某个用户在系统中的相关日志记录时,就会变得很麻烦。

一种解决的办法是采用自定义的日志格式,把用户的信息采用某种方式编码在日志记录中。这种方式的问题在于要求在每个使用日志记录器的类中,都可以访问到用户相关的信息。这样才可能在记录日志时使用。这样的条件通常是比较难以满足的。MDC 的作用是解决这个问题。

MDC 可以看成是一个与当前线程绑定的哈希表,可以往其中添加键值对。MDC 中包含的内容可以被同一线程中执行的代码所访问。当前线程的子线程会继承其父线程中的 MDC 的内容。当需要记录日志时,只需要从 MDC 中获取所需的信息即可。

MDC的内容则由程序在适当的时候保存进去。对于一个 Web 应用来说,通常是在请求被处理的最开始保存这些数据。

以log4j为例

在记录日志前,首先在 MDC 中保存了名称为username的数据。其中包含的数据可以在格式化日志记录时直接引用

public class MdcSample { 
   private static final Logger LOGGER = Logger.getLogger("mdc"); 

   public void log() { 
       // 保存共享信息到MDC
       MDC.put("username", "Alex"); 

       if (LOGGER.isInfoEnabled()) { 
           LOGGER.info("This is a message."); 
       } 
   } 
}

修改log4j配置文件的日志输出格式的,其中%X{username}表示引用MDC中username的值

log4j.appender.stdout.layout.ConversionPattern=%X{username} %d{yyyy-MM-dd HH:mm:ss}

MDC与ThreadLocal对比

在前文中的示例使用ThreadLocal来存储Correlation Id,并在打印日志时从ThreadLocal取出,写入日志

// ...
    
    String correlationId = RequestCorrelation.getId();
    LOGGER.info("start REST request to {} with correlationId {}", uri, correlationId);

// ...

这要求开发人员在打日志时必须先取Correlation Id,再写入日志内容。特别容易忘记或者出错。虽然可以封装LogUtil类,但对于遗留项目,要求全部日志输出都得改用logUtil,工程量太大,侵入太强,肯定无法推广。

使用带MDC的日志框架,只需修改日志配置文件,代码中日志输出的地方无需任何修改,是一种更低成本,更不容易出错的解决方案。更服务零侵入的要求。

零侵入微服务日志追踪(一):Correlation Id

Correlation ids is an essential feature of service-oriented/microservice platforms for monitoring, reporting and diagnostics。
Correlation ids allow distributed tracing within complex service oriented platforms, where a single request into the application can often be dealt with by multiple downstream service. Without the ability to correlate downstream service requests it can be very difficult to understand how requests are being handled within your platform.

拆分微服务后,对一个接口的请求,往往会引发对其他若干个微服务的调用。如果缺少对这一连串请求的关联信息,就很难搞清楚服务间调用关系。这一问题就是链路追踪问题。链路追踪又分APM和日志追踪。APM关注调用间的耗时,日志追踪关注。

链路追踪我们通常使用Correlation id(下文也称Trace id)是来解决这,来自客户端的一个请求的产生的服务间调用都被管理同一个Correlation id。
通过在APM数据和日志中输出该ID,实现追踪。

具体如下:
1.服务收到请求时,检查是否带有Correlation id,如果没有产生新的Correlation id,如果有则使用请求中的Correlation id
2.在调用下游其他服务时,带上这个唯一的id
3.服务在打印响应耗时的指标数据或者日志内容中输出Correlation id
4.APM系统收集所有请求监控数据,以Correlation id和父子调用关系,生产调用树及耗时
5.日志系统收集所有微服务的日志,解析出Correlation id,并按Correlation id索引,排查问题时可以按Correlation id搜索

一个J2EE实现例子

public class CorrelationHeaderFilter implements Filter {

    //...

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
            throws IOException, ServletException {

        final HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
        String currentCorrId = httpServletRequest.getHeader(RequestCorrelation.CORRELATION_ID_HEADER);

        if (!currentRequestIsAsyncDispatcher(httpServletRequest)) {
            if (currentCorrId == null) {
                // 生产新的Correlation id
                currentCorrId = UUID.randomUUID().toString();
                LOGGER.info("No correlationId found in Header. Generated : " + currentCorrId);
            } else {
                LOGGER.info("Found correlationId in Header : " + currentCorrId);
            }
            // 保存Correlation id到请求上下文
            RequestCorrelation.setId(currentCorrId);
        }

        filterChain.doFilter(httpServletRequest, servletResponse);
    }

    //...
    
}

例子中,通过filter拦截所有请求,判断http header是否有Correlation id,如果没有,则使用uuid生产新的Correlation id。最后存入RequestCorrelation中。

public class RequestCorrelation {

    public static final String CORRELATION_ID = "correlationId";

    private static final ThreadLocal id = new ThreadLocal();


    public static String getId() { return id.get(); }

    public static void setId(String correlationId) { id.set(correlationId); }
}

一般地可以使用ThreadLocal来存储Correlation id.因为使用一个线程处理一个请求的模式,可以使用threadlocal在请求中共享Correlation id。

当调用下游服务,已http方式为例,可利用http header来传递Correlation id。

@Component
// 子类隐藏Correlation Id传递细节
public class CorrelatingRestClient implements RestClient {

    private RestTemplate restTemplate = new RestTemplate();

    @Override
    public String getForString(String uri) {
        String correlationId = RequestCorrelation.getId();
        HttpHeaders httpHeaders = new HttpHeaders();
        
        // 以http header方式传递Correlation Id到下一个服务
        httpHeaders.set(RequestCorrelation.CORRELATION_ID, correlationId);

        LOGGER.info("start REST request to {} with correlationId {}", uri, correlationId);

        ResponseEntity response = restTemplate.exchange(uri, HttpMethod.GET,
                new HttpEntity(httpHeaders), String.class);
        // 日志中输出Correlation Id
        LOGGER.info("completed REST request to {} with correlationId {}", uri, correlationId);

        return response.getBody();
    }
}


// 调用示例
public String exampleMethod() {
        RestClient restClient = new CorrelatingRestClient();
        return restClient.getForString(URI_LOCATION); 
}

zerorpc通用Client实例直接调用特定server RPC方法原理

假设server端代码定义了一个RPC方法hello

import zerorpc
class HelloRPC(object):
    def hello(self, name):
        return "Hello, %s" % name

s = zerorpc.Server(HelloRPC())
s.bind("tcp://0.0.0.0:4242")
s.run()

在客户端代码如果要调用只需简单新建一个Client实例,然后直接以client.hello()调用。

import zerorpc
client = zerorpc.Client()
client.connect("tcp://127.0.0.1:4242")
print client.hello("RPC")

Client作为一个通用类,并未定义def hello(name):方法,直接以client.hello()调用是怎么做到的?答案就是利用了python的__getattr____call__,关键代码如下。

class Client(object):
    def __init__(self, ):
        pass

    def __call__(self, method, *args, **kwargs):
        # 在这连接server,序列化参数,请求,反序列化响应数据,并return
        pass

    def __getattr__(self, method):
        # 返回一个闭包,该闭包封装了self和method.
        # 当这个闭包被执行时,即x(*args, **kwargs),
        # 即self(method, *args, **kargs)被运行,
        # 即self.__call__(self, method, *args, **kwargs)
        f = lambda *args, **kwargs: self(method, *args, **kwargs)
        return f

当执行client.hello('RPC')时,会分两步,先获取hello这个属性的值,然后调用这个值。即先调用client.__getattr__('hello')查询hello这个属性的值,得到f,而f是一个闭包函数,然后调用f('RPC')

mac下配置centos的ssh登录使用RSA公私钥

1. 在本地的mac生成密钥对

OpenSSH 提供了ssh-keygen用于生成密钥对,不加任何参数调用即可:

$ ssh-keygen
Generating public/private rsa key pair.
Enter file in which to save the key (/Users/user/.ssh/id_rsa):

如果你以前没有生成过密钥对,直接回车就行(vi ~/.ssh/id_rsa检查下,避免覆盖,如果生成过,请输入新的文件名)。

Enter passphrase (empty for no passphrase): 
Enter same passphrase again: 
Your identification has been saved in /Users/user/.ssh/_rsa
Your public key has been saved in /home/user/.ssh/id_rsa.pub.
The key fingerprint is:
8a:77:ec:a1:77:42:8d:5d:ab:17:33:ac:87:06:20:3c user@mbp101
The key's randomart image is:
+--[ RSA 2048]----+
|                 |
|                 |
|   .             |
|    E .     .    |
|     o .S+ o .   |
|     . o+ o *    |
|    . o.+. + +   |
|     . +o.* o    |
|      ...+ o     |
+-----------------+

如果你不想每次连接时都被问及密码(它是用来解开特定的公钥),在创建密钥对的时候,你只须按 enter 作为密码。创建密钥对时,是否以密码加密纯粹是你的决定。如何你不将密钥加密,任何人夺得你的本地机器后,便自动拥有远程服务器的 ssh 访问权。此外,本地机器上的 root 能够访问你的密钥:但假若你不能信任 root(或者 root 已被攻占),你已经大祸临头。将密钥加密舍弃了不用密码的 ssh 服务器,来换取额外的安全,得来的就是输入密码来使用这条密钥。

注意:在这里我并没有默认使用id_rsa,因为我的id_rsa已经有了东西,所以我用了xxx_rsa,这里需要额外配置~/.ssh/config

加入

Host yongyao.li
        User xxx
        Hostname yongyao.li
        PreferredAuthentications publickey
        IdentityFile ~/.ssh/yongyaoli_rsa

使ssh能正确查找到私钥,而且我为私钥设置了密码,在登录时mac的keychain会弹出,输入后keychain会记住,不用每次都输入私钥保护密码。

2. 上传密钥

把你的公钥用scp或者sftp上传到了远程远程ssh服务器,并把公钥的内容追加到ssh服务器的 ~/.ssh/authorized_keys:

$ scp ~/.ssh/id_rsa.pub user@host:
$ ssh root@host
$ cat id_rsa.pub >> ~/.ssh/authorized_keys

3. ~/.ssh 相关文件权限

现在为本地mac的私钥设置权限:

$ chmod 700 ~/.ssh
$ chmod 600 ~/.ssh/id_rsa

设置centos服务器上的文件权限:

$ chmod 700 ~/.ssh
$ chmod 600 ~/.ssh/authorized_keys

如果 /etc/ssh/sshd_config 内的 StrictModes 被启用(缺省值),以上的权限是必须的。

4.一旦你检查过可以用密钥对来登录服务器,你可以在你的centos服务器的 /etc/ssh/sshd_conf 内加入以下设置来停用口令验证:
# 停用口令验证,强制使用密钥对

PasswordAuthentication no

重启sshd

# service sshd restart

这是换一台机器或者虚拟机进行登录,会提示以下

# ssh xxx@yongyao.li
Permission denied,  (publickey,gssapi-keyex,gssapi-with-mic)

iOS使用自定义字体

iOS 6 自带字体:iOS 6: Font list

iOS 7 自带字体:iOS 7: Font list

OS X Mavericks 自带字体:
OS X: Fonts included with Mavericks

上面两篇文档里列出的字体名字并非编程时用的fontName,只是对应的字体文件的名字。

可以使用下面的代码查系统自带字体的 fontName,在任意 UIViewController 里加上:

- (void)viewDidLoad
{
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    
    for (NSString* family in [UIFont familyNames])
    {
        NSLog(@"%@", family);
        for (NSString* name in [UIFont fontNamesForFamilyName: family])
        {
            NSLog(@"%@", name);
        }
    }
}

以下是真机 debug 在控制台打印出来的:

Thonburi
Thonburi-Bold
Thonburi
Thonburi-Light
...
Didot-Bold
Didot-Italic
Didot
DIN Alternate
DINAlternate-Bold
Bodoni 72 Smallcaps
BodoniSvtyTwoSCITCTT-Book

加入我想要在 iOS 的 UILabel 中使用 mac 中自带的Yuanti SC,怎么搞?
1.下载你想要的字体的字体文件,可以是 ttf、otf、ttc 等格式(ttc 格式需要特别注意),此处我从 mac 的系统默认字体存储目录拷贝;
2.把字体文件添加到 xcode 项目中;
2.在Info.plist添加以下代码并保存;

    
   UIAppFonts
	
		Yuanti.ttc
		WawaSC-Regular.otf
		Lantinghei.ttc
		汉仪楷体简.ttf
	
   

B261A1C3-2E8F-4006-8AFE-B4831828E225
3.在 Build Phases/Copy Bundle Resources添加字体文件;
70428009-10BC-4667-B5B7-D985179CC12F
4.使用上面 viewDidLoad 代码查找新增的 fontName,比如我添加 Yuanti.ttc 后新增了

Yuanti SC
STYuanti-SC-Light
STYuanti-SC-Bold
STYuanti-SC-Regular

最后在代码中

self.contentLabel.font = [UIFont fontWithName:@"Yuanti SC" size:16.0f];

效果:
83E5E5D7-4417-4015-BAB0-80EE6964554B

Lantinghei.tcc 下面包含8个字体。

Lantinghei TC //繁体细
FZLTXHB--B51-0 //繁体extralight
FZLTTHB--B51-0 //繁体heavy
FZLTZHB--B51-0 //繁体demibold
Lantinghei SC //简体
FZLTZHK--GBK1-0 //简体demibold
FZLTTHK--GBK1-0 //简体heavy
FZLTXHK--GBK1-0 //简体extralight

如果不适用粗体,就直接用Lantinghei SC就可以了。

ttc 字体文件是字体集合,提取 ttc 字体名方法:

-(NSArray*)customFontArrayWithPath:(NSString*)path size:(CGFloat)size
{
    CFStringRef fontPath = CFStringCreateWithCString(NULL, [path UTF8String], kCFStringEncodingUTF8);
    CFURLRef fontUrl = CFURLCreateWithFileSystemPath(NULL, fontPath, kCFURLPOSIXPathStyle, 0);
    CFArrayRef fontArray =CTFontManagerCreateFontDescriptorsFromURL(fontUrl);
    CTFontManagerRegisterFontsForURL(fontUrl, kCTFontManagerScopeNone, NULL);
    NSMutableArray *customFontArray = [NSMutableArray array];
    for (CFIndex i = 0 ; i < CFArrayGetCount(fontArray); i++){
        CTFontDescriptorRef  descriptor = CFArrayGetValueAtIndex(fontArray, i);
        CTFontRef fontRef = CTFontCreateWithFontDescriptor(descriptor, size, NULL);
        NSString *fontName = CFBridgingRelease(CTFontCopyName(fontRef, kCTFontPostScriptNameKey));
        UIFont *font = [UIFont fontWithName:fontName size:size];
        [customFontArray addObject:font];
    }
    
    return customFontArray;
}

另一种思路是直接从 ttc 提取 ttf,有待研究。

参考资料:

Tutorial: Porting fonts to the iPhone

osx文件权限中的@和+

If the file or directory has extended attributes, the permissions field printed by the -l option is followed by a ‘@’ character.
Otherwise, if the file or directory has extended security information (such as an access control list), the permissions field printed by the -l option is followed by a ‘+’ character.

显示文件[夹]:
chflags nohidden ~/Library
如想隐藏,可以在终端中执行命令:
chflags hidden ~/Library

-rw-------@   1 wingyiu  staff  626164 Aug 12  2013 Bookmarks.bak
drwxr-xr-x+   3 wingyiu  staff     102 Jan  2 20:09 Desktop
drwxr-xr-x+  34 wingyiu  staff    1156 Dec 24 19:11 Documents
drwx------+ 278 wingyiu  staff    9452 Jan  5 20:20 Downloads
drwxr-xr-x   34 wingyiu  staff    1156 Dec 23 23:40 Git
drwxr-xr-x   87 wingyiu  staff    2958 Dec 30 23:50 Github
drwx------+  70 wingyiu  staff    2380 Jan  3 17:17 Library
drwx------+   4 wingyiu  staff     136 Jan  2 20:57 Movies
drwx------+   7 wingyiu  staff     238 Oct  7  2013 Music
drwx------+ 252 wingyiu  staff    8568 Jan  5 19:57 Pictures
drwxr-xr-x+   4 wingyiu  staff     136 Sep 28  2012 Public

MBP101 Yosemite升级 SSD

本人机子为MacBook Pro (13-inch, Mid 2012),系统已经升级为OS X 10.10.1 Yosemite;光驱位和硬盘位置的SATA均为 SATA3,但硬盘仅支持 SATA2,所以协商以 SATA2速度运行。
mac check sata 3
一番 V2EX 和 Google 发现 Yosemite 引入一个叫 kext signing 的东西,默认开启,开启后会禁止使用未经苹果认证授权的第三方硬件,也会影响 TRIM 的使用。
TRIM 可以对 SSD 进行增强,比如稍微提高读写速度、使用寿命等。在这篇文章FAQ and support for using Trim Enabler in OS X Yosemite里提到了。

在旧版本系统里通常是使用 Trim Enabler 来开启Trim 的,免去敲命令行的烦恼。现在多了 kext signing,就必须先关闭 kext signing。在 Trim Enabler 3.3 以上版本已经集成了 kext signing 开关闭的功能。我用的是4.0.4。trim enabler turn off kext signing

按流程走,重启后重新打开 Trim Enabler,再开启 Trim 就可以了。

最好拿个足够大的移动硬盘做一个 Time Machine 备份,以免玩大了搞坏原硬盘丢了数据。

准备材料:
1、SSD,我采用的是 SamSung SSD 840 EVO,目前价格约850RMB;
2、光驱硬盘托架,我采用的是Nimitz OptiBay-3,购买时要注意托架的厚度,是否支持 SATA3 ;
3、可选的 USB 外置吸入式光驱盒,用来把拆下来的光驱做成外接 USB 光驱;
4、拆机工具,买2、3时有赠送。
硬盘托架和光驱盒加起来大概100+。

详细拆解更换可以参考 Zealer 王自如的视频Macbook Pro 改装 RAID

搞定后启动 Mac,对新硬盘进行初始化、格式化。

本来想用 USB 移动硬盘进行 Time Machine 备份,然后进入 Recovery HD 把备份回复到 SSD 盘上,奈何移动硬盘有很多资料,也没分区。Recovery HD 在从10.9升级到10.10时已经损坏,双系统 Win 7也进不去。只能用别的方法了。

解决方法是使用Carbon Copy Cloner,把整个机械盘的数据复制到 SSD 上。如果你的 SSD 介绍,可以选择删掉旧硬盘部分数据,所以选择性的复制。比如指复制系统和 Applications。我在清除很多无用、冗余、旧数据,把大小降到200一下,进行的整盘复制。复制花费时间较长,主要是因为机械盘读较慢,小文件过多。用了大概3个小时。复制完会提示给 SSD 创建 Recovery HD,我的旧 Recovery HD 已经损坏,且 SSD 空间所剩不足就放弃了。

ccc hd to ssd

重启后按住 option 键,选择 SSD 启动。进入系统配置,把 SSD 改为启动盘,这样每次启动就会载入 SSD 上的系统了。

change mac startup drive

关于HD和 SSD 如何组合发挥SSD 读写快容量小、HD 容量大速度慢的功效,大概有 Fusion Driver,Raid 0, Raid 1,SSD 做系统启动盘、机械盘做数据、挂在其文件系统下,比如/home。这个日后再研究。