l  前言

  本文记录了我的一次.net core 微服务架构实践经验,以及所用到的技术

l  优点

  1. 每个服务聚焦于一块业务,无论在开发阶段或是部署阶段都是独立的,更适合被各个小团队开发维护,团队对服务的整个生命周期负责,工作在独立的上下文之中。
  2. 如果某一项服务的性能达到瓶颈,我们只需要增加该服务负载节点,能够针对系统的瓶颈服务更有效的使用资源。

  3. 服务A可以使用.net实现 ,服务B可以使用java实现,技术选型灵活,系统不会长期限制在某个技术栈上。
  4. 松耦合、高内聚,代码容易理解,开发效率高,更好维护。
  5. 高可用,每个服务可以启动多个实例负载,单个实例挂了有足够的响应时间来修复  

l  缺点

  1. 系统规模庞大,运维要求高,需要devops技巧(Jenkins,Kubernetes等等)
  2. 跨服务需求需要团队之间的协作
  3. 跨服务的调用(http/rpc)增加了系统的延迟

l  Docker

  docker是目前普遍使用的容器化技术,在此架构中我们的应用程序将部署在docker容器里面,通过docker发布应用 需要先编写一个dockerfile,如下

#引入镜像 .net core 3.1
FROM mcr.microsoft.com/dotnet/core/aspnet:3.1-buster-slim AS base
#设定工作目录
WORKDIR /app
#在容器中程序使用的端口,一定要和程序启动使用的端口对应上
EXPOSE 80
#复制文件到工作目录
COPY . .
#环境变量 此变量会覆盖appsetting.json 内的同名变量
ENV Ip ""
ENV Port ""
#启动程序
ENTRYPOINT ["dotnet", "Union.UserCenter.dll"]

  docker build 命令 将我们的发布目录打包一个docker镜像,例如    docker build -t test .    ,test是镜像名称

  docker run 命令启动我们打包的镜像,例如 docker run -d -p 5002:80 --name="test1" -e Ip="192.168.0.164" -e Port="5002"  test ,-e 表示传递环境变量

  更多docker命令 请查阅:https://www.runoob.com/docker/docker-command-manual.html

  docker官网:https://www.docker.com

  

  • 部署方便:只需要一个简单的 docker run命令,就可以启动一个应用实例了
  • 部署安全:打包镜像的时候已经打包了应用所需环境,运行环境不会出现任何问题
  • 隔离性好:同一台机器我可以部署java的应用和.net的应用,互不影响
  • 快速回滚:只要镜像存在可以快速回滚到任一版本
  • 成本低:一台机器可以运行很多实例,很容易就可以实现高可用和横向扩展

  经测试docker for windows不适合部署生产环境,还是得在liunx系统上跑, .net framework 无法在docker上部署

  Docker compose :Docker官方提供的管理工具,可以简单的配置一组容器启动参数、启动顺序、依赖关系

  Kubernetes :容器数量很多之后会变得难以管理,可以引入Kubernetes对容器进行自动管理,熟练运用有一定难度,尚未使用  中文社区:https://www.kubernetes.org.cn/k8s

l  RPC 远程过程调用

  为什么要有RPC

  按照微服务设计思想,服务A只专注于服务A的业务,但是需求上肯定会有服务A需要调用服务B来完成一个业务处理的情况,使用http调用其他服务效率相对较低,所以引入了RPC。

  gRPC vs thrift  评测:https://www.cnblogs.com/softidea/p/7232035.html

  这里使用thrift,thrift 官网:http://thrift.apache.org

  Thrift 采用IDL(Interface Definition Language)来定义通用的服务接口,然后通过Thrift提供的编译器,可以将服务接口编译成不同语言编写的代码,通过这个方式来实现跨语言的功能,语法请自行百度

  

  下载thrift 代码生成器  http://thrift.apache.org/download ,thrift-0.13.0.exe 这个文件

  执行命令 thrift.exe --gen netcore xxxxxxx.thrift ,生成C# 服务接口代码

  

  引用官方提供的.net 库,可以去官网下载,找不到的可以直接 nuget引用 Examda.Thrift,这是我为了方便使用上传的

  添加生成的代码到我们的服务端里,然后自己实现 thrift文件定义的接口

using System.Threading;
using System.Threading.Tasks;
using Union.UnionInfo.Service.Interface;
using static Examda.Contract.UnionInfo.UnionInfoService; namespace Union.UnionInfo.Service
{
public class UnionInfoServiceImpl : IAsync
{
private readonly ILmMembersInfoService _lmMembersInfoService;
public UnionInfoServiceImpl(ILmMembersInfoService lmMembersInfoService)
{
_lmMembersInfoService = lmMembersInfoService;
}
//实现接口
public async Task<string> GetUnionIdAsync(string DozDomain, CancellationToken cancellationToken)
{
return (await _lmMembersInfoService.GetMembersInfoByDozDomain(DozDomain)).UnionId;
}
}
}

  添加一个类继承 IHostedService 

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System.Threading;
using System.Threading.Tasks;
using Thrift;
using Thrift.Protocols;
using Thrift.Server;
using Thrift.Transports;
using Thrift.Transports.Server; namespace Examda.Core.Rpc
{
public class RpcServiceHost : IHostedService
{
public IConfiguration Configuration { get; } public ITAsyncProcessor Processor { get; } public ILoggerFactory LoggerFactory { get; } public RpcServiceHost(IConfiguration configuration, ITAsyncProcessor processor,ILoggerFactory loggerFactory)
{
Configuration = configuration;
Processor = processor;
LoggerFactory = loggerFactory;
}
//
public virtual Task StartAsync(CancellationToken cancellationToken)
{ TServerTransport serverTransport = new TServerSocketTransport(Configuration.GetValue<int>("RpcPort")); TBinaryProtocol.Factory factory1 = new TBinaryProtocol.Factory();
TBinaryProtocol.Factory factory2 = new TBinaryProtocol.Factory(); //UnionInfoService.AsyncProcessor processor = new AsyncProcessor(new UnionInfoServiceImpl());实现的服务这里采用.net core 自带 DI注入,也可以直接实例化
TBaseServer server = new AsyncBaseServer(Processor, serverTransport, factory1, factory2, LoggerFactory); return server.ServeAsync(cancellationToken);
}
public virtual Task StopAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
}
}

  修改ConfigureServices添加如下代码

            //注入rpc服务实现实例
services.AddSingleton<ITAsyncProcessor>(provider =>
{
var lmMembersInfoService = provider.GetService<ILmMembersInfoService>();
return new AsyncProcessor(new UnionInfoServiceImpl(lmMembersInfoService));
});
//监听rpc端口
services.AddHostedService<RpcServiceHost>();

  服务端就完成了,接下来编写客户端调用,修改客户端ConfigureServices添加如下代码

            //test rpc服务
services.AddScoped(provider =>
{
var examdaConsul = provider.GetService<ExamdaConsul>();
Address address = examdaConsul.GetAddress("UnionInfo");//获取服务地址,这里我封装了,测试可以先直接写死
var tClientTransport = new TSocketClientTransport(IPAddress.Parse(address.Ip), address.Port);
var tProtocol = new TBinaryProtocol(tClientTransport);
return new UnionInfoService.Client(tProtocol);
});

  控制器内调用示例

using System.Threading;
using System.Threading.Tasks;
using Examda.Contract.UnionInfo;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging; namespace RPCCLIENT.Controllers
{
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
private static readonly string[] Summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
}; private readonly UnionInfoService.Client _rpcClient;
private readonly ILogger<WeatherForecastController> _logger; public WeatherForecastController(ILogger<WeatherForecastController> logger, UnionInfoService.Client rpcClient)
{
_logger = logger;
_rpcClient = rpcClient;
} [HttpGet]
public async Task<IActionResult> Get()
{
await _rpcClient.OpenTransportAsync(CancellationToken.None);
var order = await _rpcClient.GetUnionIdAsync("wx.hdgk.cn", CancellationToken.None);//rpc调用
return Ok(order);
}
}
}

l  服务注册与发现

  为什么要有服务注册与发现
  

  例如:服务A一开始只有一个实例,此时又启动了一个服务A的实例,但是调用服务A的服务B并不知道 服务A多了一个实例(或者少了),此时引入服务注册与发现可以让服务B得知服务A的变更情况,服务B就知道自己要调用的服务IP:端口 是多少,不需要人工干预

  常见的注册中心

  

  这里使用consul

  健康检查:consul自带健康检查,检查服务是否可用,不可用的服务将从注册中心剔除,自带的就是隔一段时间检测一下端口通不通,并且支持自行扩展健康检查,可用自己在服务内实现是否健康的逻辑,比如虽然接口是通的,但是我发现自己宿主机cpu过80%了,就返回不健康的状态

  服务注册:nuget安装consul,写一个扩展方法

        /// <summary>
/// 如果服务同时包含http,rpc调用此方法
/// </summary>
/// <param name="services"></param>
/// <param name="Configuration"></param>
/// <param name="ServiceName"></param>
/// <param name="Remark"></param>
public static void AddExamdaServiceRpc(this IServiceCollection services, IConfiguration Configuration, string ServiceName, string Remark)
{
var Ip = Configuration.GetValue<string>("Ip");
var RpcPort = Configuration.GetValue<int>("RpcPort");
var RpcAddress = $"{Ip}:{RpcPort}";
var consulClient = new ConsulClient(x => x.Address = new Uri(Configuration.GetValue<string>("ConsulUrl")));//请求注册的 Consul 地址
var httpCheck = new AgentServiceCheck()
{
DeregisterCriticalServiceAfter = TimeSpan.FromSeconds(),//服务启动多久后注册
Interval = TimeSpan.FromSeconds(),//健康检查时间间隔,或者称为心跳间隔
Timeout = TimeSpan.FromSeconds(),
TCP = RpcAddress
};
var registration = new AgentServiceRegistration()
{
Checks = new[] { httpCheck },
ID = RpcAddress,
Name = ServiceName,
Address = Ip,
Port = RpcPort,
Tags = new[] { Remark }
};
consulClient.Agent.ServiceRegister(registration).Wait();
//应用程序退出时
AppDomain.CurrentDomain.ProcessExit += (sender, e) =>
{
consulClient.Agent.ServiceDeregister(registration.ID).Wait();//consul取消注册服务
};
}

  修改ConfigureServices添加如下代码,启动

            services.AddExamdaServiceRpc(Configuration, "UnionInfo", "联盟机构信息服务");

  

  安装consul请自行百度

   服务发现与变更:调用方配置好自己需要调用的服务名称集合,然后去consul获取地址列表,然后根据需要调用的服务数量启动N个线程来轮询服务最新的地址信息,不用担心轮询造成的消耗过大,因为consul提供了Blocking Queries 阻塞查询的方式,请求发送到consul之后会在consul阻塞(30)秒,期间有变更或者到达30秒了之后才会返回地址列表,然后每一次变更之后的地址列表都会有一个新的版本号。

using Consul;
using Microsoft.Extensions.Configuration;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks; namespace Examda.Core.Consul
{ public class Address
{
public string Ip { get; set; } public int Port { get; set; }
}
/// <summary>
/// 未实现服务负载均衡,这里随机选一个
/// </summary>
public class ExamdaConsul
{
private object locker = new object();
private readonly ConsulClient _consulClient;
private IDictionary<string, List<Address>> RpcServices { get; set; }
public ExamdaConsul(IConfiguration configuration)
{
RpcServices = new Dictionary<string, List<Address>>();
_consulClient = new ConsulClient(c =>
{
c.Address = new Uri(configuration.GetValue<string>("ConsulUrl"));
});
foreach (var item in configuration.GetSection("RpcServiceClient").GetChildren().Select(x => x.Value).ToList())//遍历所需要调用的服务名称集合
{
RpcServices.Add(item, null);
var res = _consulClient.Catalog.Service(item).Result;
RpcServices[item] = res.Response.Select(x => new Address() { Ip = x.ServiceAddress, Port = x.ServicePort }).ToList();
Task.Factory.StartNew(() =>
{
var queryOptions = new QueryOptions { WaitTime = TimeSpan.FromSeconds() };//阻塞时间
queryOptions.WaitIndex = res.LastIndex;
while (true)
{
GetAgentServices(queryOptions, item);
}
});
}
}
private void GetAgentServices(QueryOptions queryOptions, string serviceName)
{
var res = _consulClient.Catalog.Service(serviceName, null, queryOptions).Result;
if (queryOptions.WaitIndex != res.LastIndex)
{
lock (locker)
{
queryOptions.WaitIndex = res.LastIndex;
var currentServices = RpcServices[serviceName];
RpcServices[serviceName] = res.Response.Select(x => new Address() { Ip = x.ServiceAddress, Port = x.ServicePort }).ToList();
}
}
}
/// <summary>
/// 获取服务可用地址
/// </summary>
/// <param name="serviceName"></param>
/// <returns></returns>
public Address GetAddress(string serviceName)
{
for (int i = ; i < ; i++)
{
Random r = new Random();
int index = r.Next(RpcServices.Count);
try
{
return RpcServices[serviceName][index];
}
catch
{ Thread.Sleep();
continue;
}
}
return null;
}
}
}

  然后注入一个ExamdaConsul类的单例,将写死的服务地址改成从consul获取

            //注入consul客户端 单例
services.AddSingleton<ExamdaConsul>();
//注入UnionInfo rpc客户端 线程单例
services.AddScoped(provider =>
{
var examdaConsul = provider.GetService<ExamdaConsul>();
Address address = examdaConsul.GetAddress("UnionInfo");//从consul获取服务地址
var tClientTransport = new TSocketClientTransport(IPAddress.Parse(address.Ip), address.Port);
var tProtocol = new TBinaryProtocol(tClientTransport);
return new UnionInfoService.Client(tProtocol);
});

  consul 官网:https://www.consul.io

l  API网关

  所有的请求都先经过网关,由转发到对应的服务,对比了 ocelot 和 Bumblebee 两个c#写的网关。选择使用了Bumblebee。

  Ocelot性能比较低,测试情况很不理想,但是文档很全面,功能集成很多,不需要自己扩展什么。

  Bumblebee 我做测试发现Bumblebee 性能很优秀,尴尬的是这个几乎没什么人用,很多功能需要自己扩展,作者官网http://beetlex.io/ Bumblebee 文档:http://doc.beetlex.io/#29322e3796694434894fc2e6e8747626

  这里使用Bumblebee ,使用方法可以看作者的文档

  健康检查:不健康的节点将不会被转发请求

  限流:例如限制某个节点最多300rps,如果此节点并发了1000个请求,大概会有700个左右请求网关会直接返回错误,不会转发到具体的服务,可以起到挡洪作用,避免节点直接挂了。

  路由:我是这么设置的 例如 http://192.168.0.164/Course/Tool/GetUserInfo ,Course一级是服务名称 tool 是服务的控制器名称 getuserinfo是方法名称

  负载均衡:服务多个节点负载,网关可以设置负载均衡策略

  

  注册到网关:暂时redis发布订阅实现,添加一个扩展方法,计划走Consul

        public static void AddExamdaService(this IServiceCollection services, IConfiguration Configuration, string ServiceName, string Remark)
{
var Ip = Configuration.GetValue<string>("Ip");
var Port = Configuration.GetValue<int>("Port");
var Address = $"http://{Ip}:{Port}";
services.AddSingleton(new Redis(Configuration.GetValue<string>("Redis")));
ServiceProvider serviceProvider = services.BuildServiceProvider();
Redis redis = serviceProvider.GetService<Redis>();
redis.Publish("ApiGetewap", JsonConvert.SerializeObject(new { Address, ServiceName, Remark }));
AppDomain.CurrentDomain.ProcessExit += (sender, e) =>
{
redis.Publish("ApiGetewapExit", JsonConvert.SerializeObject(new { Address, ServiceName, Remark }));
};
}

  网关订阅这个频道

 g = new OverrideApiGetewap();
g.HttpOptions(o =>
{
o.Port = ;
o.LogToConsole = true;
o.LogLevel = BeetleX.EventArgs.LogType.Error;
});
g.Open();
var sub = Program.redis.GetSubscriber();
//注册服务
sub.Subscribe("ApiGetewap",(chanel,message)=> {
var service = JsonConvert.DeserializeObject<Service>(message);
var route = g.Routes.NewOrGet(string.Format("^/{0}.*", service.ServiceName), service.Remark);
route.AddServer(service.Address, );
});
//服务退出
sub.Subscribe("ApiGetewapExit", (chanel, message) => {
var service = JsonConvert.DeserializeObject<Service>(message);
var route = g.Routes.NewOrGet(string.Format("^/{0}.*", service.ServiceName), service.Remark);
route.RemoveServer(service.Address);
});

  修改ConfigureServices添加如下代码,启动。这样网关也能动态的发现我们的服务了

            //注册此服务到网关
services.AddExamdaService(Configuration, "Course", "联盟我的课程服务");

  

  异常流量拉黑:例如某个ip 10s内请求数量超过300 将他拉黑 30 分钟,这里使用redis实现计数器

  自己写的简陋版本,千万不要用到生产

            //请求完成触发的事件,不会阻塞请求
g.RequestIncrement += (sender, e) =>
{
Task.Factory.StartNew(() =>
{
var db = Program.redis.GetDatabase();
var counter = db.KeyExists(e.Request.RemoteIPAddress);//判断该ip是否存在计数器
if (counter)
{
var count = db.StringIncrement(e.Request.RemoteIPAddress);//计数器加1
if (count > )
{
db.StringSet("BlackList_" + e.Request.RemoteIPAddress, "", new TimeSpan(, , ), flags: StackExchange.Redis.CommandFlags.FireAndForget);//拉黑半个小时,不等待返回值
}
}
else
{
db.StringIncrement(e.Request.RemoteIPAddress, flags: StackExchange.Redis.CommandFlags.FireAndForget);//创建计数器
db.KeyExpire(e.Request.RemoteIPAddress, new TimeSpan(, , ), flags: StackExchange.Redis.CommandFlags.FireAndForget);//设置10s过期
}
});
};
    class OverrideApiGetewap : Bumblebee.Gateway
{
//请求管道的第一个事件
protected override void OnHttpRequest(object sender, EventHttpRequestArgs e)
{
if (!e.Request.Path.Contains("/__system/bumblebee") && e.Request.Path != "/")//排除掉访问网关ui的
{
var db = Program.redis.GetDatabase();
var isBlack = db.KeyExists("BlackList_" + e.Request.RemoteIPAddress);
if (isBlack)
{
e.Response.Result(new JsonResult("你被拉黑了"));
e.Cancel = true;//取消请求
}
else
{
base.OnHttpRequest(sender, e);
}
//base.OnHttpRequest(sender, e);
}
else
{
base.OnHttpRequest(sender, e);
}
}
}

  熔断器:当某个请求转发下游服务返回错误次数或者超时次数达到阀值时自动熔断该节点,暂未实现

  接口验签:客户端请求都带上用 url时间戳 参数加密的签名,网关进行验证,确保是合法的客户端

  网关自带UI

  

  

l  链路追踪 性能监控

  Skywalking 官网:http://skywalking.apache.org/ 

  每个请求的链路,每一个步骤的耗时都可以查到,如下图的一个请求执行了很多次sql,每个步骤的sql语句都可以看到,集成很简单,使用官方提供的.net探针集成到各个服务就好了,无代码入侵。

  有一个很强大的ui界面,也可以提供报警等功能,ui可以查看到响应很慢的接口,平均响应时间,以及每个服务的关联关系,但是有个问题暂时没找到解决方案RPC链路追踪不到。

    可以自行去官方查阅使用文档

  

l  分布式日志收集框架

  实例太多了,不可能使用单机日志,需要一个分布式日志收集框架把所有日志收集到一起,可以考虑使用elk 或者 .net core 的Exceptionless

l  分布式事务

  跨服务之间调用并且涉及到事务的处理方式,暂未实现

l  配置中心

  各个实例逐个配置太麻烦了,特别是如果更改了数据库地址,每一个服务的所有实例都要改,改死去,并且重启实例也不现实,一定要支持配置热更新,试了下携程的Apollo有点消耗资源

l  CI/CD

  将源码管理做一个开发分支,一个测试分支,一个发布分支,开发只动开发分支,开发完成后提交代码,由测试合并到测试分支,并通知Jenkins生成镜像并发布到测试站点,测试通过之后由运维合并到发布分支,或手动或自动通过Jenkins发布,应该保证 测试分支与发布分支的版本能对应docker镜像仓库的每一个版本,个人见解。

记录与分享自己的一次微服务实践

以上均为个人见解,不对的地方或者好的建议欢迎来信 289501868@qq.com

demo地址 https://git.lug.ustc.edu.cn/lgdvvvv/demo

.net 微服务实践的更多相关文章

  1. 2019年微服务实践第一课,网易&谐云&蘑菇街&奥思技术大咖深度分享

    微服务的概念最早由Martin Fowler与James Lewis于2014年共同提出,核心思想是围绕业务能力组织服务,各个微服务可被独立部署,服务间是松耦合的关系,以及数据和治理的去中心化管理.微 ...

  2. QCon技术干货:个推基于Docker和Kubernetes的微服务实践

    2016年伊始,Docker无比兴盛,如今Kubernetes万人瞩目.在这个无比需要创新与速度的时代,由容器.微服务.DevOps构成的云原生席卷整个IT界.在近期举办的QCon全球软件开发大会上, ...

  3. .NET CORE微服务实践

    .NET CORE微服务实践 https://www.cnblogs.com/zengqinglei/p/9570343.html .NET CORE 实践部署架构图 实践源码:https://git ...

  4. SFDC 微服务实践之路 2016.12.10 杭州(整理)--转

    原文地址:http://mp.weixin.qq.com/s/8cC4Ewt6yPjnxdYxuNZlFQ 微服务是什么? 微服务是一种细粒度(Fine-Grain)的SOA 或许在座的高朋了解过其概 ...

  5. 微服务实践(七):从单体式架构迁移到微服务架构 - DockOne.io

    原文:微服务实践(七):从单体式架构迁移到微服务架构 - DockOne.io [编者的话]这是用微服务开发应用系列博客的第七篇也是最后一篇.第一篇中介绍了微服务架构模式,并且讨论了微服架构的优缺点: ...

  6. 微服务实践(五):微服务的事件驱动数据管理 - DockOne.io

    原文:微服务实践(五):微服务的事件驱动数据管理 - DockOne.io [编者的话]本文是使用微服务创建应用系列的第五篇文章.第一篇文章介绍了微服务架构模式,并且讨论了使用微服务的优缺点:第二和第 ...

  7. 微服务实践之路--RPC

    微服务实践之路--RPC 重点来了,本文全面阐述一下我们的RPC是怎么实现并如何使用的,跟Kubernetes和Openstack怎么结合. 在选型一文中说到我们选定的RPC框架是Apache Thr ...

  8. Openstack+Kubernetes+Docker微服务实践

    Openstack+Kubernetes+Docker微服务实践 .....   Openstack+Kubernetes+Docker微服务实践之路--选型 posted @ 2016-11-15 ...

  9. 干货 | 国内互联网公司是如何做微服务实践的?(附PPT下载)

    微服务的概念最早由Martin Fowler与James Lewis于2014年共同提出,并随着Netflix最佳实践的发布而为业界所知.如今,在国内有了大量的微服务实践案例,5月18日,网易云联合云 ...

  10. Golang微服务实践

    背景 在之前的文章<漫谈微服务>我已经简单的介绍过微服务,微服务特性是轻量级跨平台和跨语言的服务,也列举了比较了集中微服务通信的手段的利弊,本文将通过RPC通信的方式实现一个增删查Redi ...

随机推荐

  1. OGG bi-directional replication for Oracle DB

    Overview of an Active-Active Configuration Oracle GoldenGate supports an active-active bi-directiona ...

  2. [bzoj2326] [洛谷P3216] [HNOI2011] 数学作业

    想法 最初的想法就是记录当前 \(%m\) 值为cur,到下一个数时 \(cur=cur \times 10^x + i\) n这么大,那就矩阵乘法呗. 矩阵乘法使用的要点就是有一个转移矩阵会不停的用 ...

  3. Git The requested URL returned error:403

    一.问题描述 hexo部署的仓库需要换到另一个账号上,于是按照之前创建的步骤,修改了hexo安装目录下的deploy的repo地址,然而hexo s和hexo g没出错,但是hexo d时,会出错. ...

  4. 什么是“跑面”呢? - ERSS耳斯百科:您的随身移动百科

    跑面 [pǎo miàn] 跑面,是一个汉语词汇,拼音为pǎo miàn,英文名为Run-Noodles,最基本解释为人跑步去吃面,其意义还有多重深层解释. 中文名:跑面 英文名:Run-Noodle ...

  5. MEF sample

    博客里介绍ntier 基于这个框架有一个叫WAF的示例项目. 看 waf(WPF Application Framework)里面这样有句 不是很懂, This page might help you ...

  6. CSS-13-块级元素和行内元素

    <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title> ...

  7. Python使用requests发送post请求的三种方式

    1.我们使用postman进行接口测试的时候,发现POST请求方式的编码有3种,具体的编码方式如下: A:application/x-www-form-urlencoded ==最常见的post提交数 ...

  8. java操作数组转list集合删除元素报错ConcurrentModificationException

    public static void main(String[] args) { ArrayList<String> list = new ArrayList<String>( ...

  9. Oracle:imp导入dmp文件

    oracle命令行登录 sqlplus / as sysdba 创建用户 create user 用户 identified by 密码 ; 创建表空间 create tablespace 表空间名 ...

  10. CAD制图系列之椭圆画法标注

    今天我将做一个极轴是92,150的椭圆画法和标注方法 1.打开2014版本CAD制图,快捷键EL,回车: 2.自己随便定一个点 3.输入第一个值,也就是短轴--横轴(输入实际长度,不需要除以二)并且鼠 ...