Skip to content

Commit 3db63da

Browse files
neilbrownamschuma-ntap
authored andcommitted
NFSv3: handle out-of-order write replies.
NFSv3 includes pre/post wcc attributes which allow the client to determine if all changes to the file have been made by the client itself, or if any might have been made by some other client. If there are gaps in the pre/post ctime sequence it must be assumed that some other client changed the file in that gap and the local cache must be suspect. The next time the file is opened the cache should be invalidated. Since Commit 1c341b7 ("NFS: Add deferred cache invalidation for close-to-open consistency violations") in linux 5.3 the Linux client has been triggering this invalidation. The chunk in nfs_update_inode() in particularly triggers. Unfortunately Linux NFS assumes that all replies will be processed in the order sent, and will arrive in the order processed. This is not true in general. Consequently Linux NFS might ignore the wcc info in a WRITE reply because the reply is in response to a WRITE that was sent before some other request for which a reply has already been seen. This is detected by Linux using the gencount tests in nfs_inode_attr_cmp(). Also, when the gencount tests pass it is still possible that the request were processed on the server in a different order, and a gap seen in the ctime sequence might be filled in by a subsequent reply, so gaps should not immediately trigger delayed invalidation. The net result is that writing to a server and then reading the file back can result in going to the server for the read rather than serving it from cache - all because a couple of replies arrived out-of-order. This is a performance regression over kernels before 5.3, though the change in 5.3 is a correctness improvement. This has been seen with Linux writing to a Netapp server which occasionally re-orders requests. In testing the majority of requests were in-order, but a few (maybe 2 or three at a time) could be re-ordered. This patch addresses the problem by recording any gaps seen in the pre/post ctime sequence and not triggering invalidation until either there are too many gaps to fit in the table, or until there are no more active writes and the remaining gaps cannot be resolved. We allocate a table of 16 gaps on demand. If the allocation fails we revert to current behaviour which is of little cost as we are unlikely to be able to cache the writes anyway. In the table we store "start->end" pair when iversion is updated and "end<-start" pairs pre/post pairs reported by the server. Usually these exactly cancel out and so nothing is stored. When there are out-of-order replies we do store gaps and these will eventually be cancelled against later replies when this client is the only writer. If the final write is out-of-order there may be one gap remaining when the file is closed. This will be noticed and if there is precisely on gap and if the iversion can be advanced to match it, then we do so. This patch makes no attempt to handle directories correctly. The same problem potentially exists in the out-of-order replies to create/unlink requests can cause future lookup requires to be sent to the server unnecessarily. A similar scheme using the same primitives could be used to notice and handle out-of-order replies. Signed-off-by: NeilBrown <neilb@suse.de> Signed-off-by: Anna Schumaker <Anna.Schumaker@Netapp.com>
1 parent 03f5bd7 commit 3db63da

File tree

2 files changed

+144
-15
lines changed

2 files changed

+144
-15
lines changed

fs/nfs/inode.c

Lines changed: 97 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -208,11 +208,12 @@ void nfs_set_cache_invalid(struct inode *inode, unsigned long flags)
208208

209209
nfsi->cache_validity |= flags;
210210

211-
if (inode->i_mapping->nrpages == 0)
212-
nfsi->cache_validity &= ~(NFS_INO_INVALID_DATA |
213-
NFS_INO_DATA_INVAL_DEFER);
214-
else if (nfsi->cache_validity & NFS_INO_INVALID_DATA)
215-
nfsi->cache_validity &= ~NFS_INO_DATA_INVAL_DEFER;
211+
if (inode->i_mapping->nrpages == 0) {
212+
nfsi->cache_validity &= ~NFS_INO_INVALID_DATA;
213+
nfs_ooo_clear(nfsi);
214+
} else if (nfsi->cache_validity & NFS_INO_INVALID_DATA) {
215+
nfs_ooo_clear(nfsi);
216+
}
216217
trace_nfs_set_cache_invalid(inode, 0);
217218
}
218219
EXPORT_SYMBOL_GPL(nfs_set_cache_invalid);
@@ -677,9 +678,10 @@ static int nfs_vmtruncate(struct inode * inode, loff_t offset)
677678
trace_nfs_size_truncate(inode, offset);
678679
i_size_write(inode, offset);
679680
/* Optimisation */
680-
if (offset == 0)
681-
NFS_I(inode)->cache_validity &= ~(NFS_INO_INVALID_DATA |
682-
NFS_INO_DATA_INVAL_DEFER);
681+
if (offset == 0) {
682+
NFS_I(inode)->cache_validity &= ~NFS_INO_INVALID_DATA;
683+
nfs_ooo_clear(NFS_I(inode));
684+
}
683685
NFS_I(inode)->cache_validity &= ~NFS_INO_INVALID_SIZE;
684686

685687
spin_unlock(&inode->i_lock);
@@ -1109,7 +1111,7 @@ void nfs_inode_attach_open_context(struct nfs_open_context *ctx)
11091111

11101112
spin_lock(&inode->i_lock);
11111113
if (list_empty(&nfsi->open_files) &&
1112-
(nfsi->cache_validity & NFS_INO_DATA_INVAL_DEFER))
1114+
nfs_ooo_test(nfsi))
11131115
nfs_set_cache_invalid(inode, NFS_INO_INVALID_DATA |
11141116
NFS_INO_REVAL_FORCED);
11151117
list_add_tail_rcu(&ctx->list, &nfsi->open_files);
@@ -1353,8 +1355,8 @@ int nfs_clear_invalid_mapping(struct address_space *mapping)
13531355

13541356
set_bit(NFS_INO_INVALIDATING, bitlock);
13551357
smp_wmb();
1356-
nfsi->cache_validity &=
1357-
~(NFS_INO_INVALID_DATA | NFS_INO_DATA_INVAL_DEFER);
1358+
nfsi->cache_validity &= ~NFS_INO_INVALID_DATA;
1359+
nfs_ooo_clear(nfsi);
13581360
spin_unlock(&inode->i_lock);
13591361
trace_nfs_invalidate_mapping_enter(inode);
13601362
ret = nfs_invalidate_mapping(inode, mapping);
@@ -1816,6 +1818,66 @@ static int nfs_inode_finish_partial_attr_update(const struct nfs_fattr *fattr,
18161818
return 0;
18171819
}
18181820

1821+
static void nfs_ooo_merge(struct nfs_inode *nfsi,
1822+
u64 start, u64 end)
1823+
{
1824+
int i, cnt;
1825+
1826+
if (nfsi->cache_validity & NFS_INO_DATA_INVAL_DEFER)
1827+
/* No point merging anything */
1828+
return;
1829+
1830+
if (!nfsi->ooo) {
1831+
nfsi->ooo = kmalloc(sizeof(*nfsi->ooo), GFP_ATOMIC);
1832+
if (!nfsi->ooo) {
1833+
nfsi->cache_validity |= NFS_INO_DATA_INVAL_DEFER;
1834+
return;
1835+
}
1836+
nfsi->ooo->cnt = 0;
1837+
}
1838+
1839+
/* add this range, merging if possible */
1840+
cnt = nfsi->ooo->cnt;
1841+
for (i = 0; i < cnt; i++) {
1842+
if (end == nfsi->ooo->gap[i].start)
1843+
end = nfsi->ooo->gap[i].end;
1844+
else if (start == nfsi->ooo->gap[i].end)
1845+
start = nfsi->ooo->gap[i].start;
1846+
else
1847+
continue;
1848+
/* Remove 'i' from table and loop to insert the new range */
1849+
cnt -= 1;
1850+
nfsi->ooo->gap[i] = nfsi->ooo->gap[cnt];
1851+
i = -1;
1852+
}
1853+
if (start != end) {
1854+
if (cnt >= ARRAY_SIZE(nfsi->ooo->gap)) {
1855+
nfsi->cache_validity |= NFS_INO_DATA_INVAL_DEFER;
1856+
kfree(nfsi->ooo);
1857+
nfsi->ooo = NULL;
1858+
return;
1859+
}
1860+
nfsi->ooo->gap[cnt].start = start;
1861+
nfsi->ooo->gap[cnt].end = end;
1862+
cnt += 1;
1863+
}
1864+
nfsi->ooo->cnt = cnt;
1865+
}
1866+
1867+
static void nfs_ooo_record(struct nfs_inode *nfsi,
1868+
struct nfs_fattr *fattr)
1869+
{
1870+
/* This reply was out-of-order, so record in the
1871+
* pre/post change id, possibly cancelling
1872+
* gaps created when iversion was jumpped forward.
1873+
*/
1874+
if ((fattr->valid & NFS_ATTR_FATTR_CHANGE) &&
1875+
(fattr->valid & NFS_ATTR_FATTR_PRECHANGE))
1876+
nfs_ooo_merge(nfsi,
1877+
fattr->change_attr,
1878+
fattr->pre_change_attr);
1879+
}
1880+
18191881
static int nfs_refresh_inode_locked(struct inode *inode,
18201882
struct nfs_fattr *fattr)
18211883
{
@@ -1826,8 +1888,12 @@ static int nfs_refresh_inode_locked(struct inode *inode,
18261888

18271889
if (attr_cmp > 0 || nfs_inode_finish_partial_attr_update(fattr, inode))
18281890
ret = nfs_update_inode(inode, fattr);
1829-
else if (attr_cmp == 0)
1830-
ret = nfs_check_inode_attributes(inode, fattr);
1891+
else {
1892+
nfs_ooo_record(NFS_I(inode), fattr);
1893+
1894+
if (attr_cmp == 0)
1895+
ret = nfs_check_inode_attributes(inode, fattr);
1896+
}
18311897

18321898
trace_nfs_refresh_inode_exit(inode, ret);
18331899
return ret;
@@ -1918,6 +1984,8 @@ int nfs_post_op_update_inode_force_wcc_locked(struct inode *inode, struct nfs_fa
19181984
if (attr_cmp < 0)
19191985
return 0;
19201986
if ((fattr->valid & NFS_ATTR_FATTR) == 0 || !attr_cmp) {
1987+
/* Record the pre/post change info before clearing PRECHANGE */
1988+
nfs_ooo_record(NFS_I(inode), fattr);
19211989
fattr->valid &= ~(NFS_ATTR_FATTR_PRECHANGE
19221990
| NFS_ATTR_FATTR_PRESIZE
19231991
| NFS_ATTR_FATTR_PREMTIME
@@ -2072,6 +2140,15 @@ static int nfs_update_inode(struct inode *inode, struct nfs_fattr *fattr)
20722140

20732141
/* More cache consistency checks */
20742142
if (fattr->valid & NFS_ATTR_FATTR_CHANGE) {
2143+
if (!have_writers && nfsi->ooo && nfsi->ooo->cnt == 1 &&
2144+
nfsi->ooo->gap[0].end == inode_peek_iversion_raw(inode)) {
2145+
/* There is one remaining gap that hasn't been
2146+
* merged into iversion - do that now.
2147+
*/
2148+
inode_set_iversion_raw(inode, nfsi->ooo->gap[0].start);
2149+
kfree(nfsi->ooo);
2150+
nfsi->ooo = NULL;
2151+
}
20752152
if (!inode_eq_iversion_raw(inode, fattr->change_attr)) {
20762153
/* Could it be a race with writeback? */
20772154
if (!(have_writers || have_delegation)) {
@@ -2093,8 +2170,11 @@ static int nfs_update_inode(struct inode *inode, struct nfs_fattr *fattr)
20932170
dprintk("NFS: change_attr change on server for file %s/%ld\n",
20942171
inode->i_sb->s_id,
20952172
inode->i_ino);
2096-
} else if (!have_delegation)
2097-
nfsi->cache_validity |= NFS_INO_DATA_INVAL_DEFER;
2173+
} else if (!have_delegation) {
2174+
nfs_ooo_record(nfsi, fattr);
2175+
nfs_ooo_merge(nfsi, inode_peek_iversion_raw(inode),
2176+
fattr->change_attr);
2177+
}
20982178
inode_set_iversion_raw(inode, fattr->change_attr);
20992179
}
21002180
} else {
@@ -2248,6 +2328,7 @@ struct inode *nfs_alloc_inode(struct super_block *sb)
22482328
return NULL;
22492329
nfsi->flags = 0UL;
22502330
nfsi->cache_validity = 0UL;
2331+
nfsi->ooo = NULL;
22512332
#if IS_ENABLED(CONFIG_NFS_V4)
22522333
nfsi->nfs4_acl = NULL;
22532334
#endif /* CONFIG_NFS_V4 */
@@ -2262,6 +2343,7 @@ EXPORT_SYMBOL_GPL(nfs_alloc_inode);
22622343

22632344
void nfs_free_inode(struct inode *inode)
22642345
{
2346+
kfree(NFS_I(inode)->ooo);
22652347
kmem_cache_free(nfs_inode_cachep, NFS_I(inode));
22662348
}
22672349
EXPORT_SYMBOL_GPL(nfs_free_inode);

include/linux/nfs_fs.h

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,39 @@ struct nfs_inode {
195195
/* Open contexts for shared mmap writes */
196196
struct list_head open_files;
197197

198+
/* Keep track of out-of-order replies.
199+
* The ooo array contains start/end pairs of
200+
* numbers from the changeid sequence when
201+
* the inode's iversion has been updated.
202+
* It also contains end/start pair (i.e. reverse order)
203+
* of sections of the changeid sequence that have
204+
* been seen in replies from the server.
205+
* Normally these should match and when both
206+
* A:B and B:A are found in ooo, they are both removed.
207+
* And if a reply with A:B causes an iversion update
208+
* of A:B, then neither are added.
209+
* When a reply has pre_change that doesn't match
210+
* iversion, then the changeid pair and any consequent
211+
* change in iversion ARE added. Later replies
212+
* might fill in the gaps, or possibly a gap is caused
213+
* by a change from another client.
214+
* When a file or directory is opened, if the ooo table
215+
* is not empty, then we assume the gaps were due to
216+
* another client and we invalidate the cached data.
217+
*
218+
* We can only track a limited number of concurrent gaps.
219+
* Currently that limit is 16.
220+
* We allocate the table on demand. If there is insufficient
221+
* memory, then we probably cannot cache the file anyway
222+
* so there is no loss.
223+
*/
224+
struct {
225+
int cnt;
226+
struct {
227+
u64 start, end;
228+
} gap[16];
229+
} *ooo;
230+
198231
#if IS_ENABLED(CONFIG_NFS_V4)
199232
struct nfs4_cached_acl *nfs4_acl;
200233
/* NFSv4 state */
@@ -612,6 +645,20 @@ nfs_fileid_to_ino_t(u64 fileid)
612645
return ino;
613646
}
614647

648+
static inline void nfs_ooo_clear(struct nfs_inode *nfsi)
649+
{
650+
nfsi->cache_validity &= ~NFS_INO_DATA_INVAL_DEFER;
651+
kfree(nfsi->ooo);
652+
nfsi->ooo = NULL;
653+
}
654+
655+
static inline bool nfs_ooo_test(struct nfs_inode *nfsi)
656+
{
657+
return (nfsi->cache_validity & NFS_INO_DATA_INVAL_DEFER) ||
658+
(nfsi->ooo && nfsi->ooo->cnt > 0);
659+
660+
}
661+
615662
#define NFS_JUKEBOX_RETRY_TIME (5 * HZ)
616663

617664
/* We need to block new opens while a file is being unlinked.

0 commit comments

Comments
 (0)