终极事务处理(XTP,Hekaton)——万能大招?

时间:2023-04-10 23:08:36
终极事务处理(XTP,Hekaton)——万能大招?

在SQL Server 2014里,微软引入了终极事务处理(Extreme Transaction Processing),即大家熟知的Hekaton。我在网上围观了一些文档,写这篇文章,希望可以让大家更好的理解Hekaton,它的局限性,还有它惊艳的全新内存数据库技术。这篇文章会通过下面几个方面来讲解Hekaton:

  1. 概况
  2. 可扩展性(Scalability)
  3. 局限性(Limitations)

1.概况

让我们从XTP的简洁概况开始。像XTP这样的内存数据库技术首要目标非常明确:尽可能高效的使用我们现有的服务器硬件。我们来看当下的现代服务器系统硬件,你会发现下列问题/局限性:

  • 传统存储(机械硬盘)非常缓慢,企业若准备SSD存储非常昂贵。另一方面主要内存(RAM)却非常便宜,只要花100美元就可以配置到64GB内存,这可是标准版的SQL Server的最大支持内存数。
  • CPU速度很难再提升。现在我们困在了3-4GHZ,再快点不太可能(当你尝试超频时你就会有这个体会)。
  • 传统的关系型数据库管理系统(RDBMS)不能线性扩展,主要是因为内部的锁,阻塞和*(Locking, Blocking, and Latching)机制(数据结构的内存里锁,当它们被读写访问时)。

因此为了克服这些限制,你需要这样的技术:

  1. 使用RAM将数据全部存在内存里来克服传统旋转硬盘(机械硬盘)的速度限制。
  2. 尽可能划算的使用当下有速度限制的CPU,使用尽可能少的CPU指令数,来实现近可能快的去执行关系数据库管理系统(RDBMS)的查询。
  3. 当对你的关系数据库管理系统(RDBMS)执行读/写操作时,完全避免锁/阻塞,和闩锁(Locking/Blocking, and Latching)。

这3点就是SQL Server 2014里终极事务处理(Extreme Transaction Processing )的3大支柱:

终极事务处理(XTP,Hekaton)——万能大招?

使用XTP你可以在内存里缓存整个表(即所谓的内存优化表(Memory Optimized Tables)),存储过程可以编译为本机C代码,而且对于内存优化表,锁/阻塞和闩锁(Locking/Blocking, and Latching)这些机制都是完全没有的,因为XTP是基于乐观的多版本并发控制(Multi Version Concurrency Control ,MVCC)的。我们来详细看下这3大支柱。

内存中的存储(In-Memory Storage)

对于服务器系统,内存越来越便宜了。你只要花几百美元就可以装备你的服务器为64G内存,64GB内存可是SQL Server标准版本支持的最大内存。因此XTP表(即内存优化表)是完全存在内存里的。从SQL Server角度来看,内存表的所有数据存在于FILESTREAM文件组,在SQL Server启动时,它们从文件组读取,然后在起飞时重建你的所有索引。

这也给你的目标恢复时间(Recovery Time Objective,RTO)带来巨大压力,因为一启动你所有索引都被重建完成后,你的数据库才是在线状态。你的FILESTREAM文件组所存储的存储系统速度,因此也会直接影响目标恢复时间。因此你可以在FILESTREAM文件组里放置多个容器,这样的话你可以在启动期间分散I/O到多个存储系统,从而让你的数据库尽快进入在线状态。

在CTP1里,XTP只支持所谓的Hash-Indexes,它是在内存里完全存储在哈希表里。SQL Server目前能查找和扫描Hash-Indexes。从CTP2起,微软会引入所谓的Range Indexes,这样可以是你的范围查询非常,非常快。Range Indexes是基于所谓的Bw-Tree

每个内存优化表也是编译为本地C代码。对每个表你都会得到一个DLL,这个是通过cl.exe编译的(微软C语言编译器,SQL Server 2014 自带)。生成的DLL然后载入sqlservr.exe 的进程空间,这个可以在sys.dm_os_loaded_modules里看到。编译本身在独立的线程里完成,这就是说你眼疾手快的话,你可以在任务管理器里看到cl.exe。下面代码给你展示了描述你的表的典型C语言代码:

 struct hkt_277576027
{
struct HkSixteenByteData hkc_1;
__int64 hkc_5;
long hkc_2;
long hkc_3;
long hkc_4;
};
struct hkis_27757602700002
{
struct HkSixteenByteData hkc_1;
};
struct hkif_27757602700002
{
struct HkSixteenByteData hkc_1;
};
__int64 CompareSKeyToRow_27757602700002(
struct HkSearchKey const* hkArg0,
struct HkRow const* hkArg1)
{
struct hkis_27757602700002* arg0 = ((struct hkis_27757602700002*)hkArg0);
struct hkt_277576027* arg1 = ((struct hkt_277576027*)hkArg1);
__int64 ret;
ret = (CompareKeys_guid((arg0->hkc_1), (arg1->hkc_1)));
return ret;
}
__int64 CompareRowToRow_27757602700002(
struct HkRow const* hkArg0,
struct HkRow const* hkArg1)
{
struct hkt_277576027* arg0 = ((struct hkt_277576027*)hkArg0);
struct hkt_277576027* arg1 = ((struct hkt_277576027*)hkArg1);
__int64 ret;
ret = (CompareKeys_guid((arg0->hkc_1), (arg1->hkc_1)));
return ret;
}

可以看到,代码本身并不直观,但你可以通过C的结构使用来看出你的表结构是如何被描述的。XTP的好处是内存优化表是完全自然集成到SQL Server关系引擎的其余部分。因此你可以使用传统的T-SQL代码查询这些表,可以进行备份/还原,还有集成HA/DR技术——微软在集成领域做了大量的伟大工作。除了内存中存储外,内存优化表也完全锁/阻塞,和闩锁,因为XTP是基于乐观的多版本并发控制(MVCC)原则。在无锁/闩锁数据结构部分我们会继续讨论这个。

你需要注意的最重要事实是:你应该将性能最重要的表移入内存,不是你所有的数据库。在接下来可扩展性部分里,我们会谈到哪些情况使用XTP是有意义的。通常你会把你数据库的95%使用基于传统磁盘的表存储,剩下的5%可以用内存优化表存储。

本地编译(Native Compilation)

微软申明当前硬件系统的第一个问题是:传统的,旋转的存储太慢。对此将表数据在内存中存储。第2个问题需要申明的是:当下处理器的时钟频率卡在了3-4GHz。我们不能再快了,因为会引入散热问题。因此当前时钟周期必须尽可能有效的管理。这是当下T-SQL实现的巨大问题,因为T-SQL只是一个解释性的语言。

在查询优化期间,SQL Server的查询优化器生成所谓的查询树,在执行期间,查询树从头运算符到所有的树节点被解读。这会引入大量的额外CPU指令,会在SQL Server里的每个执行计划里执行。另外每个运算符(即所谓的迭代器(Iterator))是以C++类实现的,这就意味在执行各个运算符时,会用到所谓的虚拟函数调用(Virtual Function Calls )。虚拟方法调用根据需要执行的CPU指令又是很占资源的。总之,在运行期间,执行计划被解读时,生成大量的CPU指令,即意味着当前的CPU没有高效的使用。你在浪费宝贵的CPU周期,可以用另外更好的方式来使用,从而提速你的整个工作。

因为查询引擎内的这些原因和限制,SQL Server引入XTP所谓的 本机编译的存储过程(Natively Compiled Stored Procedures)。背后的思路很简单:存储过程的整体编译为本地C语言代码,结果又是生成DLL,然后载入sqlservr.exe 的进程空间。因此在执行期间不需要解读,虚拟函数调用完全消除。这样的话做同样数量的工作却需要很少的CPU指令,这就意味着你的工作输出量会更高,因为在可用的CPU周期里可以做更多的工作。

在2013年的北美TechEd上指出,对于一些特定的存储过程,需要的CPU指令可以从1000000下降到近4000。想象下这个性能提升:25倍的性能提升!当我们谈到当前XPT里的局限时,你会发现,这个提升并不是免费的……下面的代码展示的是一个简单存储过程的典型C语言代码:

 HRESULT hkp_309576141(
struct HkProcContext* context,
union HkValue valueArray[],
unsigned char* nullArray)
{
unsigned long yc = ;
long var_2 = (- - );
unsigned char var_isnull_2 = ;
HRESULT hr = ;
{
var_2 = ;
var_isnull_2 = ;
}
yc = (yc + );
{
while ()
{
unsigned char result_7;
unsigned char result_isnull_7;
result_7 = ;
result_isnull_7 = ;
if ((! var_isnull_2))
{
result_7 = (var_2 < );
}
else
{
result_isnull_7 = ;
}
if ((result_isnull_7 || (! result_7)))
{
goto l_5;
}
hr = (YieldCheck(context, yc, ));
if ((FAILED(hr)))
{
goto l_1;
}
yc = ;
{
long expr_9;
long expr_10;
long expr_11;
__int64 expr_12;
struct hkt_277576027* rec2_17 = ;
unsigned char freeRow_17 = ;
short rowLength;
static wchar_t const hkl_18[] =
{
,
,
,
,
,
,
};
static wchar_t const hkl_19[] =
{
,
,
,
,
,
,
,
,
};
static wchar_t const hkl_20[] =
{
,
,
,
,
,
,
,
,
,
};
static wchar_t const hkl_21[] =
{
,
,
,
,
,
,
};
static wchar_t const hkl_22[] =
{
,
,
,
,
,
,
,
,
};
static wchar_t const hkl_23[] =
{
,
,
,
,
,
,
,
,
,
,
,
,
};
static wchar_t const hkl_24[] =
{
,
,
,
,
,
,
};
static wchar_t const hkl_25[] =
{
,
,
,
,
,
,
,
,
};
static wchar_t const hkl_26[] =
{
,
,
,
,
,
,
,
,
,
,
,
};
static wchar_t const hkl_27[] =
{
,
,
,
,
,
,
};
static wchar_t const hkl_28[] =
{
,
,
,
,
,
,
,
,
};
static wchar_t const hkl_29[] =
{
,
,
,
,
,
,
,
,
,
,
};
static wchar_t const hkl_30[] =
{
,
,
,
,
,
,
};
static wchar_t const hkl_31[] =
{
,
,
,
,
,
,
,
,
};
static wchar_t const hkl_32[] =
{
,
,
,
,
,
,
,
};
goto l_16;
l_16:;
expr_9 = ;
expr_10 = ;
expr_11 = ;
expr_12 = ;
goto l_15;
l_15:;
rowLength = sizeof(struct hkt_277576027);
hr = (HkRowAlloc((context->Transaction), (Tables[]), rowLength, ((struct HkRow**)(&rec2_17))));
if ((FAILED(hr)))
{
goto l_8;
}
freeRow_17 = ;
if ((! (nullArray[])))
{
(rec2_17->hkc_1) = ((valueArray[]).SixteenByteData);
}
else
{
hr = -;
if ((FAILED(hr)))
{
{
CreateError((context->ErrorObject), hr, , , hkl_20, , hkl_19, hkl_18);
}
if ((FAILED(hr)))
{
goto l_8;
}
}
}
(rec2_17->hkc_2) = expr_9;
(rec2_17->hkc_3) = expr_10;
(rec2_17->hkc_4) = expr_11;
(rec2_17->hkc_5) = expr_12;
freeRow_17 = ;
hr = (HkTableInsert((Tables[]), (context->Transaction), ((struct HkRow*)rec2_17)));
if ((FAILED(hr)))
{
goto l_8;
}
goto l_13;
l_13:;
goto l_14;
l_14:;
hr = (HkRefreshStatementId((context->Transaction)));
if ((FAILED(hr)))
{
goto l_8;
}
l_8:;
if ((FAILED(hr)))
{
if (freeRow_17)
{
HkTableReleaseUnusedRow(((struct HkRow*)rec2_17), (Tables[]), (context->Transaction));
}
SetLineNumberForError((context->ErrorObject), );
goto l_1;
}
}
yc = (yc + );
{
__int64 temp_34;
if ((! var_isnull_2))
{
temp_34 = (((__int64)var_2) + ((__int64)));
if ((temp_34 < (- - )))
{
hr = -;
{
hr = (CreateError((context->ErrorObject), hr, , , ));
}
if ((FAILED(hr)))
{
goto l_33;
}
}
if ((temp_34 > ))
{
hr = -;
{
hr = (CreateError((context->ErrorObject), hr, , , ));
}
if ((FAILED(hr)))
{
goto l_33;
}
}
var_2 = ((long)temp_34);
var_isnull_2 = ;
}
else
{
var_isnull_2 = ;
}
l_33:;
if ((FAILED(hr)))
{
SetLineNumberForError((context->ErrorObject), );
goto l_1;
}
}
yc = (yc + );
}
l_5:;
}
yc = (yc + );
((valueArray[]).SignedIntData) = ;
l_1:;
return hr;
}

你可以看到有很多的GOTO语句,很容易让人想到是面条式代码(spaghetti code)。但这个不是我们讨论的范围……

终极事务处理(XTP,Hekaton)——万能大招?

无锁/闩锁数据结构(Lock/Latch Free Data Structures)

在我们刚才讨论XTP里的内存存储数据时,我已经说过对内存优化表,SQL Server是以无锁/闩锁数据结构实现的。这意味着当想要读写你的数据时,没有锁/闩锁涉及到的等待。在传统的像SQL Server这样关系数据库管理系统(RDBMS)里,写操作需要排它锁(Exclusive Locks (X) ),读操作需要共享锁(Shared Locks (S) )。2个锁是互斥的。

这就是说读阻塞写,写阻塞读。共享锁能把持多久是通过不同的事务隔离级别控制的。这个方式称为悲观并发控制(Pessimistic Concurrency)。随着SQL Server 2005的发布,微软引入了新的并发模式:乐观并发控制(Optimistic Concurrency)。使用乐观并发控制,读操作不再需要共享锁。直接从TempDb永驻的版本存储里读取。

使用新的隔离级别Read Committed Snapshot Isolation (RCSI) ,你回退到语句开始后不再有效的记录版本;使用隔离级别Snapshot Isolation,你回退到事务开始后不再有效的记录版本,这意味这在Snapshot Isolation里你可以重复的读。

终极事务处理(XTP,Hekaton)——万能大招?

设置这些新的隔离级别会大大促进你的整个工作量。但还有问题必须解决:

写还是需要排它锁,意味这并行写操作还是会相互阻塞。

当访问内存中的数据时(数据页,索引页),这些结构必须被闩锁,意味着它们只能被单线程访问。那是传统多线程并发问题,需要以此方式(闩锁)来解决。

因此XTP引入了基于多版本并发控制(Multi Version Concurrency Control ,MVCC)原则。使用MVCC就没有锁(甚至没有排它锁)和闩锁。写不会相互阻塞,因为哈希索引不是建立在页上的,数据访问是无闩锁的(内部是用哈希桶的哈希表来存储的)。在内存里不再有阻塞。当你使用XTP时,意味着卓越的吞吐量保证。但你的内存里访问没有闩锁时,你的性能瓶颈将转移,主要移向事务日志,在下个讨论XTP扩展性时我们会谈到。

MVCC的一个副作用是所谓的写-写冲突(Writer-Writer conflict。你可以在XTP里对同个记录进行多个写操作,它们不会阻塞。第一个写会胜出,其他所有并发的写事务会失败。这意味着你需要修改你的代码来捕获这个特定错误,然后重试你的事务。这和死锁处理是一样的。如果在你程序里已经有死锁的实现方式,应该很容易对你的终端用户处理写-写冲突。

2.可扩展性(Scalability)

现在你应该大致理解了XTP的主要概念和背后的原因,但最大的问题是,在哪些情况下才可以使用XTP呢。我认为XTP不是一个随处可以部署的技术。你需要一个特殊情景来使用XTP才有意义。请相信我:我们现在所面临的大多数SQL Server问题,基本是索引问题,或者是硬件的错误配置问题(尤其是SANS领域)。

当你面对这样的问题时,我绝不推荐升级到XTP。一定要先分析下根源,在第一步就从根源解决问题。XTP应该是你最后才考虑的解决方法,因为当你的部分数据库使用XTP时,你的问题分析方法就会完全不一样了。对于XTP,你会遇到大量的各种限制。XTP是贼快(我可以说是TMD的快),但不是个历史奇迹(all-time wonder),不是每个地方都适用的。

微软推出XTP主要是为了克服加锁竞争(Latch Contention)问题。刚才你已经看到,当你访问数据页进行读写活动时,加锁竞争在内存里总会发生。当你的工作量越来越大,到了某个时间点,你就引入了加锁竞争,因为单线程访问内存里的这些页。这里最常见的例子就是最后页插入加锁竞争(Last Page Insert Latch Contention)

这个问题很容易重现:按照最佳实践创建一个聚集键,这个键使用自增长来避免在聚集索引里的硬页分裂。你的工作量不会延伸——相信我!这里的问题是:在INSERT语句期间,在你的聚集索引里只有一个热块——最后页。下图展示了这个现象:

终极事务处理(XTP,Hekaton)——万能大招?

从图中可以看到,SQL Server需要横穿聚集索引的右手,从索引根页下至叶子层来在聚集索引的机翼后缘插入新的记录。因此在叶子层你有单线程访问叶子页,这就意味着单线程插入(Single-Thread INSERT)操作。这会大大伤及你的性能。下图展示了使用INT IDENTITY列的表里的简单INSERT语句(自增长,导致最后页插入加锁竞争!),我使用ostress.exe程序(来自微软RML工具的一部分)模拟不同用户在16核的机器上的不同性能表现。

终极事务处理(XTP,Hekaton)——万能大招?

从图中可以看到,随着用户的增加,工作量在逐步下降,你的闩锁等待增加——这就是使用自增长值的最后页插入加锁竞争(Last Page Insert Latch Contention)。当访问在内存里的索引页和数据页时,这个竞争就会发生,因为闩锁。

有几个方式可以克服最后页插入加锁竞争:

  • 使用随机聚集键,例如UNIQUEIDENTIFIER 在整个聚集索引叶子层分布式插入
  • 实行哈希分区(Implement Hash Partitioning)

当你使用UNIQUEIDENTIFIER 作为你的聚集键时,首先你就会觉得自己做错了,但你的工作吞吐量却大幅度上升了。哈希分区是你另一个可以部署的选项。哈希分区意味着你为每个CPU内核创建不同的分区,使用取模运算符在不同的分区间,你的配分函数(Partition Function)分发记录。下图展示了这个方法:

终极事务处理(XTP,Hekaton)——万能大招?

你通过在不同分区里的不同B-Tree结构分发INSERT语句,因此你就可以并行在表里执行INSERT语句。但这个也是有缺点的,你需要SQL Server的企业版,用了这个分区,你的表就不能重新分区,你就不能有效使用分区消除(Partition Elimination)。下图是对应的性能提升展示:

终极事务处理(XTP,Hekaton)——万能大招?

从图中可以看到,当用户达到64个前都是平稳延伸的,在128个用户后,再次发生最后页插入加锁竞争,吞吐量再次下降。

现在假设我们启用内存优化表,性能会发生如何的改变?表的生产力会贼快——XTP真是的TMD的快!因为没有锁和闩锁!我们可以看下SQL Server批量请求时资源使用情况:

终极事务处理(XTP,Hekaton)——万能大招?

在这个情况下,我可以执行200个用户的本地编译存储过程里的INSERT语句,可以接收25500批量请求/秒——0工作等待,0数据I/O!所有的一切都在内存里发生。但是:这次测试都是在虚拟机里执行。虚拟机有8个内核,分配了20G的内存,虚拟机和相关的SQL Server文件都存储在PCI-E上的SSD上。

我现在碰到的XTP的生产力瓶颈在哪里呢?这个不容易马上知道答案。首先你要考虑的是还是你的聚集键。使用XTP并不支持IDENTITY列。因此微软建议使用序列对象(Sequence Object)。序列是完美的,你可以使用缓存,但是XTP是TMD的太快了,你马上就碰上了SQL Server里的序列生成器(Sequence Generator)的竞争。SQL Server在你主数据文件的第132页保存序列值。第132也是系统表sysobjvalues的一部分。当你读写那个页时,SQL Server又会闩锁那个页,你的闩锁竞争又回来了,但只在你的数据酷的不同领域上。你不能避免这个页的闩锁竞争,因为系统表还是存储在传统磁盘的表上。因此从这个角度来说序列并不是XTP的最佳解决方法,如果你想无限延伸你的工作量。

因此让我们再次回到老朋友UNIQUEIDENTIFIER 这里。当生成UNIQUEIDENTIFIER 不会有任何竞争,因为生成是通过算法的。不好的是:函数NEWID()在SQL Server 2014的CTP1里并不支持。但这个也没关系,你可以在存储过程里写入,在存储过程里生成UNIQUEIDENTIFIER ,通过变量值传给本地编译的存储过程。问题解决,这样的话我可以把工作量提升至25500 批量请求/秒。从我的测试可以看出,UNIQUEIDENTIFIER 比序列更好,因为没有需要协调和闩锁的共享资源。这个是我的最大收获。

因此现在的问题就是什么限制了25500 批量请求/秒的工作量?2个主要东西:事务日志和CPU使用率!我们首先来看看事务日志。在XTP里,微软对此做了大量的优化工作,例如没有UNDO记录。微软尝试使事务日志量最小化来优化事务日志的写入。写得少,做得快。为了克服事务日志限制,XTP提供给你2类不同的内存优化表:

  • SCHEMA_AND_DATA
  • SCHEMA_ONLY

SCHEMA_AND_DATA意味着表架构和数据都永驻,因此只要你的事务一提交,XTP就要把事务日志记录写入事务日志。在事务执行期间,XTP从不把事务日志记录写入事务日志,因为你随时可能回滚事务。SCHEMA_ONLY表数据的修改不进行日志记录,并且表中的数据不保留在磁盘上:当你重启你的SQL Server,只有空表,嗯???你要慎重考虑使用这个选项,什么时候是可以用的,什么时候是不行的。微软建议下列2个场景可以使用这个选项:

  • ASP.NET会话状态数据库
  • 提取转换加载(ETL)场景

ASP.NET会话状态数据库可以使用SCHEMA_ONLY选项,因为你这里不保存关键数据。会话状态是关于你网站用户的信息。对于ETL场景也同样可以使用SCHEMA_ONLY选项,因为即使失败也很容易重建数据。使用SCHEMA_ONLY选项,我可以获得25500 批量请求/秒的工作量。使用SCHEMA_AND_DATA就成为了主要瓶颈,只能获得15000 批量请求/秒。我说过,我的测试环境的虚拟机是运行在PCI-E上的SSD上(事务日志,数据文件,虚拟机),换做实体的纯金属服务器会更好。

当你使用SCHEMA_AND_DATA部署你的内存优化表,你还有一个东西:极快的事务日志——和往常一样!当你使用SCHEMA_AND_DATA,CPU会称为你的瓶颈,因为没别的了。从刚才的图中你可以看到虚拟机里CPU基本运行在85%。当我把所有都部署在实体服务器上是,我觉得会加倍它的工作量。因为我只分配16核中的8核给虚拟机。在主机有50%的平均CPU占用,因此加倍工作量应该不是问题。那应该是在很低成本机器上却有50000批量请求/秒的工作量……

3.局限性(Limitations)

到目前位置,XTP的一切都看起来很棒,它应该是SQL Server里特定问题的最佳解决方案。但是XTP也有很大的代价——一大堆的局限性,尤其是现在的CTP1。下面列出部分,更多可以查看微软在线帮助:

  • 差异备份不支持
  • 行大小限制为8kb
  • 内存优化表不能truncate
  • NEWID()尚未实现
  • 用SCHEMA_AND_DATA部署的内存优化表必须要有主键
  • ALTER TABLE/ALTER PROCEDURE不支持
  • 外键(Foreign-Keys)不支持
  • LOB数据类型不支持
  • 本地编译的存储过程不会重编译。由于统计信息改变,不重编译会导致很糟的性能
  • 在内存优化的存储过程里不能访问存放在传统硬盘(机械硬盘)里的表
  • 整个表的定义只能在一个CREATE TABLE里描述(包括索引和约束)
  • 你不能从别的数据库往内存优化表里直接插入数据,你需要一个中间表。这在刚才提到的ETL场景里会是个大问题
  • ……

除了这些局限性外,目前的CTP1版本里的XTP还是有BUG的,数据库居然崩溃了,也不能进行还原……不要问我如何重现这个BUG。

结论

XTP在SQL Server里的确是快速发展起来的技术。你需要考虑的唯一事情就是:当你基于XTP部署解决方案时,为你的事务日志准备尽可能快的存储系统吧,这个会大幅度降低系统瓶颈!希望这篇文章可以帮你很好的了解XTP,也希望你在阅读的时候,和我一样享受这个撰写过程。感谢您的阅读!

参考文章:

https://www.sqlpassion.at/archive/2013/08/12/extreme-transaction-processing-xtp-hekaton-the-solution-to-everything/